import { getItemDirection, getPixelSize } from "../utils";
import BaseFoundation, { DefaultAdapter } from '../../base/foundation';
import { ResizeStartCallback, ResizeCallback, ResizeEventType } from "../types";
import { adjustNewSize, judgeConstraint, getOffset } from "../utils";
import { debounce } from "lodash";
export interface ResizeHandlerAdapter
, S = Record> extends DefaultAdapter {
registerEvents: () => void;
unregisterEvents: () => void
}
export class ResizeHandlerFoundation
, S = Record> extends BaseFoundation, P, S> {
constructor(adapter: ResizeHandlerAdapter) {
super({ ...adapter });
}
init(): void {
this._adapter.registerEvents();
}
destroy(): void {
this._adapter.unregisterEvents();
}
}
export interface ResizeItemAdapter
, S = Record> extends DefaultAdapter {
}
export class ResizeItemFoundation
, S = Record> extends BaseFoundation, P, S> {
constructor(adapter: ResizeItemAdapter) {
super({ ...adapter });
}
init(): void {
}
destroy(): void {
}
}
export interface ResizeGroupAdapter
, S = Record> extends DefaultAdapter {
getGroupRef: () => HTMLDivElement | null;
getItem: (index: number) => HTMLDivElement;
getItemCount: () => number;
getHandler: (index: number) => HTMLDivElement;
getHandlerCount: () => number;
getItemMin: (index: number) => string;
getItemMax: (index: number) => string;
getItemStart: (index: number) => ResizeStartCallback;
getItemChange: (index: number) => ResizeCallback;
getItemEnd: (index: number) => ResizeCallback;
getItemDefaultSize: (index: number) => string | number;
registerEvents: (type: ResizeEventType) => void;
unregisterEvents: (type: ResizeEventType) => void
}
export class ResizeGroupFoundation
, S = Record> extends BaseFoundation, P, S> {
constructor(adapter: ResizeGroupAdapter) {
super({ ...adapter });
}
get groupRef(): HTMLDivElement | null {
return this._adapter.getGroupRef();
}
get groupSize(): number {
const { direction } = this.getProps();
let groupSize = direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
return groupSize;
}
direction: 'horizontal' | 'vertical'
itemMinusMap: Map; // 这个是为了给handler留出空间,方便维护每一个item的size为cal(percent% - minus)
totalMinus: number;
itemPercentMap: Map; // 内部维护一个百分比数组,消除浮点计算误差
type?: ResizeEventType;
init(): void {
this.direction = this.getProp('direction');
this.itemMinusMap = new Map();
this.itemPercentMap = new Map();
this.initSpace();
}
get window(): Window | null {
return this.groupRef.ownerDocument.defaultView as Window ?? null;
}
registerEvents = () => {
this._adapter.registerEvents(this.type);
}
unregisterEvents = () => {
this._adapter.unregisterEvents(this.type);
}
onResizeStart = (handlerIndex: number, e: MouseEvent | Touch, type: ResizeEventType) => { // handler ref
this.type = type;
let { clientX, clientY } = e;
let lastItem = this._adapter.getItem(handlerIndex), nextItem = this._adapter.getItem(handlerIndex + 1);
let lastOffset: number, nextOffset: number;
// offset caused by padding and border
const lastStyle = this.window.getComputedStyle(lastItem);
const nextStyle = this.window.getComputedStyle(nextItem);
lastOffset = getOffset(lastStyle, this.direction) + this.itemMinusMap.get(handlerIndex);
nextOffset = getOffset(nextStyle, this.direction) + this.itemMinusMap.get(handlerIndex + 1);
let lastItemSize = (this.direction === 'horizontal' ? lastItem.offsetWidth : lastItem.offsetHeight) + this.itemMinusMap.get(handlerIndex),
nextItemSize = (this.direction === 'horizontal' ? nextItem.offsetWidth : nextItem.offsetHeight) + this.itemMinusMap.get(handlerIndex + 1);
const states = this.getStates();
this.setState({
isResizing: true,
originalPosition: {
x: clientX,
y: clientY,
lastItemSize,
nextItemSize,
lastOffset,
nextOffset,
},
backgroundStyle: {
...states.backgroundStyle,
cursor: this.window.getComputedStyle(e.target as HTMLElement).cursor || 'auto',
},
curHandler: handlerIndex
} as any);
this.registerEvents();
let lastStart = this._adapter.getItemStart(handlerIndex),
nextStart = this._adapter.getItemStart(handlerIndex + 1);
let [lastDir, nextDir] = getItemDirection(this.direction);
if (lastStart) {
lastStart(e, lastDir as any);
}
if (nextStart) {
nextStart(e, nextDir as any);
}
}
onMouseMove = (e: MouseEvent) => {
this.onResizing(e);
}
onTouchMove = (e: TouchEvent) => {
// prevent page move in mobile
e.preventDefault();
this.onResizing(e);
}
onResizing = (e: MouseEvent | TouchEvent) => {
const state = this.getStates();
if (!state.isResizing) {
return;
}
const { curHandler, originalPosition } = state;
let { x: initX, y: initY, lastItemSize, nextItemSize, lastOffset, nextOffset } = originalPosition;
let { clientX, clientY } = this.type === 'mouse' ? e : (e as any).targetTouches[0];
const props = this.getProps();
const { direction } = props;
let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
let parentSize = this.groupSize;
let delta = direction === 'horizontal' ? (clientX - initX) : (clientY - initY);
let lastNewSize = lastItemSize + delta;
let nextNewSize = nextItemSize - delta;
// 判断是否超出限制
let lastFlag = judgeConstraint(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), parentSize, lastOffset),
nextFlag = judgeConstraint(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), parentSize, nextOffset);
if (lastFlag) {
lastNewSize = adjustNewSize(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), parentSize, lastOffset);
nextNewSize = lastItemSize + nextItemSize - lastNewSize;
}
if (nextFlag) {
nextNewSize = adjustNewSize(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), parentSize, nextOffset);
lastNewSize = lastItemSize + nextItemSize - nextNewSize;
}
let lastItemPercent = this.itemPercentMap.get(curHandler),
nextItemPercent = this.itemPercentMap.get(curHandler + 1);
let lastNewPercent = (lastNewSize) / parentSize * 100;
let nextNewPercent = lastItemPercent + nextItemPercent - lastNewPercent; // 消除浮点误差
this.itemPercentMap.set(curHandler, lastNewPercent);
this.itemPercentMap.set(curHandler + 1, nextNewPercent);
if (direction === 'horizontal') {
lastItem.style.width = `calc(${lastNewPercent}% - ${this.itemMinusMap.get(curHandler)}px)`;
nextItem.style.width = `calc(${nextNewPercent}% - ${this.itemMinusMap.get(curHandler + 1)}px)`;
} else if (direction === 'vertical') {
lastItem.style.height = `calc(${lastNewPercent}% - ${this.itemMinusMap.get(curHandler)}px)`;
nextItem.style.height = `calc(${nextNewPercent}% - ${this.itemMinusMap.get(curHandler + 1)}px)`;
}
let lastFunc = this._adapter.getItemChange(curHandler),
nextFunc = this._adapter.getItemChange(curHandler + 1);
let [lastDir, nextDir] = getItemDirection(this.direction);
if (lastFunc) {
lastFunc( { width: lastItem.offsetWidth, height: lastItem.offsetHeight }, e, lastDir as any);
}
if (nextFunc) {
nextFunc( { width: nextItem.offsetWidth, height: nextItem.offsetHeight }, e, nextDir as any);
}
}
onResizeEnd = (e: MouseEvent | TouchEvent) => {
const { curHandler } = this.getStates();
let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
let lastFunc = this._adapter.getItemEnd(curHandler),
nextFunc = this._adapter.getItemEnd(curHandler + 1);
let [lastDir, nextDir] = getItemDirection(this.direction);
if (lastFunc) {
lastFunc( { width: lastItem.offsetWidth, height: lastItem.offsetHeight }, e, lastDir as any);
}
if (nextFunc) {
nextFunc( { width: nextItem.offsetWidth, height: nextItem.offsetHeight }, e, nextDir as any);
}
this.setState({
isResizing: false,
curHandler: null
} as any);
this.unregisterEvents();
}
initSpace = () => {
const props = this.getProps();
const { direction } = props;
// calculate accurate space for group item
let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
let parentSize = this.groupSize;
this.totalMinus = 0;
for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
handlerSizes[i] = handlerSize;
this.totalMinus += handlerSize;
}
// allocate size for items which don't have default size
let totalSizePercent = 0;
let undefineLoc: Map = new Map(), undefinedTotal = 0; // proportion
for (let i = 0; i < this._adapter.getItemCount(); i++) {
if (i === 0) {
this.itemMinusMap.set(i, handlerSizes[i] / 2);
} else if (i === this._adapter.getItemCount() - 1) {
this.itemMinusMap.set(i, handlerSizes[i - 1] / 2);
} else {
this.itemMinusMap.set(i, handlerSizes[i - 1] / 2 + handlerSizes[i] / 2);
}
const child = this._adapter.getItem(i);
let minSize = this._adapter.getItemMin(i), maxSize = this._adapter.getItemMax(i);
let minSizePercent = minSize ? getPixelSize(minSize, parentSize) / parentSize * 100 : 0,
maxSizePercent = maxSize ? getPixelSize(maxSize, parentSize) / parentSize * 100 : 100;
if (minSizePercent > maxSizePercent) {
console.warn('[Semi ResizableItem]: min size bigger than max size');
}
let defaultSize = this._adapter.getItemDefaultSize(i);
if (defaultSize) {
let itemSizePercent: number;
if (typeof defaultSize === 'string') {
if (defaultSize.endsWith('%')) {
itemSizePercent = parseFloat(defaultSize.slice(0, -1));
this.itemPercentMap.set(i, itemSizePercent);
} else if (defaultSize.endsWith('px')) {
itemSizePercent = parseFloat(defaultSize.slice(0, -2)) / parentSize * 100;
this.itemPercentMap.set(i, itemSizePercent);
} else if (/^-?\d+(\.\d+)?$/.test(defaultSize)) {
// 仅由数字组成,表示按比例分配剩下空间
undefineLoc.set(i, parseFloat(defaultSize));
undefinedTotal += parseFloat(defaultSize);
continue;
}
} else if (typeof defaultSize === 'number') {
undefineLoc.set(i, defaultSize);
undefinedTotal += defaultSize;
continue;
}
totalSizePercent += itemSizePercent;
if (direction === 'horizontal') {
child.style.width = `calc(${itemSizePercent}% - ${this.itemMinusMap.get(i)}px)`;
} else {
child.style.height = `calc(${itemSizePercent}% - ${this.itemMinusMap.get(i)}px)`;
}
if (itemSizePercent < minSizePercent) {
console.warn('[Semi ResizableGroup]: item size smaller than min size');
}
if (itemSizePercent > maxSizePercent) {
console.warn('[Semi ResizableGroup]: item size bigger than max size');
}
} else {
undefineLoc.set(i, 1);
undefinedTotal += 1;
}
}
let undefineSizePercent = 100 - totalSizePercent;
if (totalSizePercent > 100) {
console.warn('[Semi ResizableGroup]: total Size bigger than 100%');
undefineSizePercent = 10; // 如果总和超过100%,则保留10%的空间均分给未定义的item
}
undefineLoc.forEach((value, key) => {
const child = this._adapter.getItem(key);
const percent = value / undefinedTotal * undefineSizePercent;
this.itemPercentMap.set(key, percent);
if (direction === 'horizontal') {
child.style.width = `calc(${percent}% - ${this.itemMinusMap.get(key)}px)`;
} else {
child.style.height = `calc(${percent}% - ${this.itemMinusMap.get(key)}px)`;
}
});
}
ensureConstraint: () => void = debounce(() => {
// 浏览器拖拽时保证px值最大最小仍生效
const { direction } = this.getProps();
const itemCount = this._adapter.getItemCount();
let continueFlag = true;
for (let i = 0; i < itemCount; i++) {
const child = this._adapter.getItem(i);
const childSize = direction === 'horizontal' ? child.offsetWidth : child.offsetHeight;
// 判断由非鼠标拖拽导致item的size变化过程中是否有超出限制的情况
const childFlag = judgeConstraint(childSize, this._adapter.getItemMin(i), this._adapter.getItemMax(i), this.groupSize, this.itemMinusMap.get(i));
if (childFlag) {
const childNewSize = adjustNewSize(childSize, this._adapter.getItemMin(i), this._adapter.getItemMax(i), this.groupSize, this.itemMinusMap.get(i));
for (let j = i + 1; j < itemCount; j++) {
// 找到下一个没有超出限制的item
const item = this._adapter.getItem(j);
const itemSize = direction === 'horizontal' ? item.offsetWidth : item.offsetHeight;
const itemFlag = judgeConstraint(itemSize, this._adapter.getItemMin(j), this._adapter.getItemMax(j), this.groupSize, this.itemMinusMap.get(j));
if (!itemFlag) {
let childPercent = this.itemPercentMap.get(i),
itemPercent = this.itemPercentMap.get(j);
let childNewPercent = childNewSize / this.groupSize * 100;
let itemNewPercent = childPercent + itemPercent - childNewPercent;
this.itemPercentMap.set(i, childNewPercent);
this.itemPercentMap.set(j, itemNewPercent);
if (direction === 'horizontal') {
child.style.width = `calc(${childNewPercent}% - ${this.itemMinusMap.get(i)}px)`;
item.style.width = `calc(${itemNewPercent}% - ${this.itemMinusMap.get(j)}px)`;
} else {
child.style.height = `calc(${childNewPercent}% - ${this.itemMinusMap.get(i)}px)`;
item.style.height = `calc(${itemNewPercent}% - ${this.itemMinusMap.get(j)}px)`;
}
break;
} else {
if (j === itemCount - 1) {
continueFlag = false;
console.warn('[Semi ResizableGroup]: no enough space to adjust min/max size');
}
}
}
}
if (!continueFlag) {
break;
}
}
}, 200)
destroy(): void {
}
}