1
0

index.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { getItemDirection, getPixelSize } from "../utils";
  2. import BaseFoundation, { DefaultAdapter } from '../../base/foundation';
  3. import { ResizeStartCallback, ResizeCallback } from "../types";
  4. import { adjustNewSize, judgeConstraint, getOffset } from "../utils";
  5. import { debounce } from "lodash";
  6. export interface ResizeHandlerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  7. registerEvents: () => void;
  8. unregisterEvents: () => void
  9. }
  10. export class ResizeHandlerFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizeHandlerAdapter<P, S>, P, S> {
  11. constructor(adapter: ResizeHandlerAdapter<P, S>) {
  12. super({ ...adapter });
  13. }
  14. init(): void {
  15. this._adapter.registerEvents();
  16. }
  17. destroy(): void {
  18. this._adapter.unregisterEvents();
  19. }
  20. }
  21. export interface ResizeItemAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  22. }
  23. export class ResizeItemFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizeItemAdapter<P, S>, P, S> {
  24. constructor(adapter: ResizeItemAdapter<P, S>) {
  25. super({ ...adapter });
  26. }
  27. init(): void {
  28. }
  29. destroy(): void {
  30. }
  31. }
  32. export interface ResizeGroupAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  33. getGroupRef: () => HTMLDivElement | null;
  34. getItem: (index: number) => HTMLDivElement;
  35. getItemCount: () => number;
  36. getHandler: (index: number) => HTMLDivElement;
  37. getHandlerCount: () => number;
  38. getItemMin: (index: number) => string;
  39. getItemMax: (index: number) => string;
  40. getItemStart: (index: number) => ResizeStartCallback;
  41. getItemChange: (index: number) => ResizeCallback;
  42. getItemEnd: (index: number) => ResizeCallback;
  43. getItemDefaultSize: (index: number) => string | number;
  44. registerEvents: () => void;
  45. unregisterEvents: () => void
  46. }
  47. export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizeGroupAdapter<P, S>, P, S> {
  48. constructor(adapter: ResizeGroupAdapter<P, S>) {
  49. super({ ...adapter });
  50. }
  51. get groupRef(): HTMLDivElement | null {
  52. return this._adapter.getGroupRef();
  53. }
  54. get groupSize(): number {
  55. const { direction } = this.getProps();
  56. let groupSize = direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
  57. return groupSize;
  58. }
  59. direction: 'horizontal' | 'vertical'
  60. itemMinusMap: Map<number, number>; // 这个是为了给handler留出空间,方便维护每一个item的size为cal(percent% - minus)
  61. totalMinus: number;
  62. itemPercentMap: Map<number, number>; // 内部维护一个百分比数组,消除浮点计算误差
  63. init(): void {
  64. this.direction = this.getProp('direction');
  65. this.itemMinusMap = new Map();
  66. this.itemPercentMap = new Map();
  67. this.initSpace();
  68. }
  69. get window(): Window | null {
  70. return this.groupRef.ownerDocument.defaultView as Window ?? null;
  71. }
  72. registerEvents = () => {
  73. this._adapter.registerEvents();
  74. }
  75. unregisterEvents = () => {
  76. this._adapter.unregisterEvents();
  77. }
  78. onResizeStart = (handlerIndex: number, e: MouseEvent) => { // handler ref
  79. let { clientX, clientY } = e;
  80. let lastItem = this._adapter.getItem(handlerIndex), nextItem = this._adapter.getItem(handlerIndex + 1);
  81. let lastOffset: number, nextOffset: number;
  82. // offset caused by padding and border
  83. const lastStyle = this.window.getComputedStyle(lastItem);
  84. const nextStyle = this.window.getComputedStyle(nextItem);
  85. lastOffset = getOffset(lastStyle, this.direction) + this.itemMinusMap.get(handlerIndex);
  86. nextOffset = getOffset(nextStyle, this.direction) + this.itemMinusMap.get(handlerIndex + 1);
  87. let lastItemSize = (this.direction === 'horizontal' ? lastItem.offsetWidth : lastItem.offsetHeight) + this.itemMinusMap.get(handlerIndex),
  88. nextItemSize = (this.direction === 'horizontal' ? nextItem.offsetWidth : nextItem.offsetHeight) + this.itemMinusMap.get(handlerIndex + 1);
  89. const states = this.getStates();
  90. this.setState({
  91. isResizing: true,
  92. originalPosition: {
  93. x: clientX,
  94. y: clientY,
  95. lastItemSize,
  96. nextItemSize,
  97. lastOffset,
  98. nextOffset,
  99. },
  100. backgroundStyle: {
  101. ...states.backgroundStyle,
  102. cursor: this.window.getComputedStyle(e.target as HTMLElement).cursor || 'auto',
  103. },
  104. curHandler: handlerIndex
  105. } as any);
  106. this.registerEvents();
  107. let lastStart = this._adapter.getItemStart(handlerIndex),
  108. nextStart = this._adapter.getItemStart(handlerIndex + 1);
  109. let [lastDir, nextDir] = getItemDirection(this.direction);
  110. if (lastStart) {
  111. lastStart(e, lastDir as any);
  112. }
  113. if (nextStart) {
  114. nextStart(e, nextDir as any);
  115. }
  116. }
  117. onResizing = (e: MouseEvent) => {
  118. const state = this.getStates();
  119. if (!state.isResizing) {
  120. return;
  121. }
  122. const { curHandler, originalPosition } = state;
  123. let { x: initX, y: initY, lastItemSize, nextItemSize, lastOffset, nextOffset } = originalPosition;
  124. let { clientX, clientY } = e;
  125. const props = this.getProps();
  126. const { direction } = props;
  127. let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
  128. let parentSize = this.groupSize;
  129. let delta = direction === 'horizontal' ? (clientX - initX) : (clientY - initY);
  130. let lastNewSize = lastItemSize + delta;
  131. let nextNewSize = nextItemSize - delta;
  132. // 判断是否超出限制
  133. let lastFlag = judgeConstraint(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), parentSize, lastOffset),
  134. nextFlag = judgeConstraint(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), parentSize, nextOffset);
  135. if (lastFlag) {
  136. lastNewSize = adjustNewSize(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), parentSize, lastOffset);
  137. nextNewSize = lastItemSize + nextItemSize - lastNewSize;
  138. }
  139. if (nextFlag) {
  140. nextNewSize = adjustNewSize(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), parentSize, nextOffset);
  141. lastNewSize = lastItemSize + nextItemSize - nextNewSize;
  142. }
  143. let lastItemPercent = this.itemPercentMap.get(curHandler),
  144. nextItemPercent = this.itemPercentMap.get(curHandler + 1);
  145. let lastNewPercent = (lastNewSize) / parentSize * 100;
  146. let nextNewPercent = lastItemPercent + nextItemPercent - lastNewPercent; // 消除浮点误差
  147. this.itemPercentMap.set(curHandler, lastNewPercent);
  148. this.itemPercentMap.set(curHandler + 1, nextNewPercent);
  149. if (direction === 'horizontal') {
  150. lastItem.style.width = `calc(${lastNewPercent}% - ${this.itemMinusMap.get(curHandler)}px)`;
  151. nextItem.style.width = `calc(${nextNewPercent}% - ${this.itemMinusMap.get(curHandler + 1)}px)`;
  152. } else if (direction === 'vertical') {
  153. lastItem.style.height = `calc(${lastNewPercent}% - ${this.itemMinusMap.get(curHandler)}px)`;
  154. nextItem.style.height = `calc(${nextNewPercent}% - ${this.itemMinusMap.get(curHandler + 1)}px)`;
  155. }
  156. let lastFunc = this._adapter.getItemChange(curHandler),
  157. nextFunc = this._adapter.getItemChange(curHandler + 1);
  158. let [lastDir, nextDir] = getItemDirection(this.direction);
  159. if (lastFunc) {
  160. lastFunc( { width: lastItem.offsetWidth, height: lastItem.offsetHeight }, e, lastDir as any);
  161. }
  162. if (nextFunc) {
  163. nextFunc( { width: nextItem.offsetWidth, height: nextItem.offsetHeight }, e, nextDir as any);
  164. }
  165. }
  166. onResizeEnd = (e: MouseEvent) => {
  167. const { curHandler } = this.getStates();
  168. let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
  169. let lastFunc = this._adapter.getItemEnd(curHandler),
  170. nextFunc = this._adapter.getItemEnd(curHandler + 1);
  171. let [lastDir, nextDir] = getItemDirection(this.direction);
  172. if (lastFunc) {
  173. lastFunc( { width: lastItem.offsetWidth, height: lastItem.offsetHeight }, e, lastDir as any);
  174. }
  175. if (nextFunc) {
  176. nextFunc( { width: nextItem.offsetWidth, height: nextItem.offsetHeight }, e, nextDir as any);
  177. }
  178. this.setState({
  179. isResizing: false,
  180. curHandler: null
  181. } as any);
  182. this.unregisterEvents();
  183. }
  184. initSpace = () => {
  185. const props = this.getProps();
  186. const { direction } = props;
  187. // calculate accurate space for group item
  188. let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
  189. let parentSize = this.groupSize;
  190. this.totalMinus = 0;
  191. for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
  192. let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
  193. handlerSizes[i] = handlerSize;
  194. this.totalMinus += handlerSize;
  195. }
  196. // allocate size for items which don't have default size
  197. let totalSizePercent = 0;
  198. let undefineLoc: Map<number, number> = new Map(), undefinedTotal = 0; // proportion
  199. for (let i = 0; i < this._adapter.getItemCount(); i++) {
  200. if (i === 0) {
  201. this.itemMinusMap.set(i, handlerSizes[i] / 2);
  202. } else if (i === this._adapter.getItemCount() - 1) {
  203. this.itemMinusMap.set(i, handlerSizes[i - 1] / 2);
  204. } else {
  205. this.itemMinusMap.set(i, handlerSizes[i - 1] / 2 + handlerSizes[i] / 2);
  206. }
  207. const child = this._adapter.getItem(i);
  208. let minSize = this._adapter.getItemMin(i), maxSize = this._adapter.getItemMax(i);
  209. let minSizePercent = minSize ? getPixelSize(minSize, parentSize) / parentSize * 100 : 0,
  210. maxSizePercent = maxSize ? getPixelSize(maxSize, parentSize) / parentSize * 100 : 100;
  211. if (minSizePercent > maxSizePercent) {
  212. console.warn('[Semi ResizableItem]: min size bigger than max size');
  213. }
  214. let defaultSize = this._adapter.getItemDefaultSize(i);
  215. if (defaultSize) {
  216. let itemSizePercent: number;
  217. if (typeof defaultSize === 'string') {
  218. if (defaultSize.endsWith('%')) {
  219. itemSizePercent = parseFloat(defaultSize.slice(0, -1));
  220. this.itemPercentMap.set(i, itemSizePercent);
  221. } else if (defaultSize.endsWith('px')) {
  222. itemSizePercent = parseFloat(defaultSize.slice(0, -2)) / parentSize * 100;
  223. this.itemPercentMap.set(i, itemSizePercent);
  224. } else if (/^-?\d+(\.\d+)?$/.test(defaultSize)) {
  225. // 仅由数字组成,表示按比例分配剩下空间
  226. undefineLoc.set(i, parseFloat(defaultSize));
  227. undefinedTotal += parseFloat(defaultSize);
  228. continue;
  229. }
  230. } else if (typeof defaultSize === 'number') {
  231. undefineLoc.set(i, defaultSize);
  232. undefinedTotal += defaultSize;
  233. continue;
  234. }
  235. totalSizePercent += itemSizePercent;
  236. if (direction === 'horizontal') {
  237. child.style.width = `calc(${itemSizePercent}% - ${this.itemMinusMap.get(i)}px)`;
  238. } else {
  239. child.style.height = `calc(${itemSizePercent}% - ${this.itemMinusMap.get(i)}px)`;
  240. }
  241. if (itemSizePercent < minSizePercent) {
  242. console.warn('[Semi ResizableGroup]: item size smaller than min size');
  243. }
  244. if (itemSizePercent > maxSizePercent) {
  245. console.warn('[Semi ResizableGroup]: item size bigger than max size');
  246. }
  247. } else {
  248. undefineLoc.set(i, 1);
  249. undefinedTotal += 1;
  250. }
  251. }
  252. let undefineSizePercent = 100 - totalSizePercent;
  253. if (totalSizePercent > 100) {
  254. console.warn('[Semi ResizableGroup]: total Size bigger than 100%');
  255. undefineSizePercent = 10; // 如果总和超过100%,则保留10%的空间均分给未定义的item
  256. }
  257. undefineLoc.forEach((value, key) => {
  258. const child = this._adapter.getItem(key);
  259. const percent = value / undefinedTotal * undefineSizePercent;
  260. this.itemPercentMap.set(key, percent);
  261. if (direction === 'horizontal') {
  262. child.style.width = `calc(${percent}% - ${this.itemMinusMap.get(key)}px)`;
  263. } else {
  264. child.style.height = `calc(${percent}% - ${this.itemMinusMap.get(key)}px)`;
  265. }
  266. });
  267. }
  268. ensureConstraint = debounce(() => {
  269. // 浏览器拖拽时保证px值最大最小仍生效
  270. const { direction } = this.getProps();
  271. const itemCount = this._adapter.getItemCount();
  272. let continueFlag = true;
  273. for (let i = 0; i < itemCount; i++) {
  274. const child = this._adapter.getItem(i);
  275. const childSize = direction === 'horizontal' ? child.offsetWidth : child.offsetHeight;
  276. // 判断由非鼠标拖拽导致item的size变化过程中是否有超出限制的情况
  277. const childFlag = judgeConstraint(childSize, this._adapter.getItemMin(i), this._adapter.getItemMax(i), this.groupSize, this.itemMinusMap.get(i));
  278. if (childFlag) {
  279. const childNewSize = adjustNewSize(childSize, this._adapter.getItemMin(i), this._adapter.getItemMax(i), this.groupSize, this.itemMinusMap.get(i));
  280. for (let j = i + 1; j < itemCount; j++) {
  281. // 找到下一个没有超出限制的item
  282. const item = this._adapter.getItem(j);
  283. const itemSize = direction === 'horizontal' ? item.offsetWidth : item.offsetHeight;
  284. const itemFlag = judgeConstraint(itemSize, this._adapter.getItemMin(j), this._adapter.getItemMax(j), this.groupSize, this.itemMinusMap.get(j));
  285. if (!itemFlag) {
  286. let childPercent = this.itemPercentMap.get(i),
  287. itemPercent = this.itemPercentMap.get(j);
  288. let childNewPercent = childNewSize / this.groupSize * 100;
  289. let itemNewPercent = childPercent + itemPercent - childNewPercent;
  290. this.itemPercentMap.set(i, childNewPercent);
  291. this.itemPercentMap.set(j, itemNewPercent);
  292. if (direction === 'horizontal') {
  293. child.style.width = `calc(${childNewPercent}% - ${this.itemMinusMap.get(i)}px)`;
  294. item.style.width = `calc(${itemNewPercent}% - ${this.itemMinusMap.get(j)}px)`;
  295. } else {
  296. child.style.height = `calc(${childNewPercent}% - ${this.itemMinusMap.get(i)}px)`;
  297. item.style.height = `calc(${itemNewPercent}% - ${this.itemMinusMap.get(j)}px)`;
  298. }
  299. break;
  300. } else {
  301. if (j === itemCount - 1) {
  302. continueFlag = false;
  303. console.warn('[Semi ResizableGroup]: no enough space to adjust min/max size');
  304. }
  305. }
  306. }
  307. }
  308. if (!continueFlag) {
  309. break;
  310. }
  311. }
  312. }, 200)
  313. destroy(): void {
  314. }
  315. }