react-moveable
Version:
A React Component that create Moveable, Draggable, Resizable, Scalable, Rotatable, Warpable, Pinchable, Groupable.
733 lines (655 loc) • 22.3 kB
text/typescript
import { PREFIX, isWebkit } from "./consts";
import { prefixNames } from "framework-utils";
import { splitBracket, isUndefined, isObject, splitUnit, IObject } from "@daybrush/utils";
import {
multiply, invert,
convertCSStoMatrix, convertMatrixtoCSS,
convertDimension, createIdentityMatrix,
createOriginMatrix, convertPositionMatrix, caculate,
multiplies,
minus,
getOrigin,
createScaleMatrix,
plus,
getRad,
} from "@moveable/matrix";
import MoveableManager from "./MoveableManager";
import { MoveableManagerState } from "./types";
export function multiply2(pos1: number[], pos2: number[]) {
return [
pos1[0] * pos2[0],
pos1[1] * pos2[1],
];
}
export function prefix(...classNames: string[]) {
return prefixNames(PREFIX, ...classNames);
}
export function createIdentityMatrix3() {
return createIdentityMatrix(3);
}
export function getTransform(target: SVGElement | HTMLElement, isInit: true): number[];
export function getTransform(target: SVGElement | HTMLElement, isInit?: false): "none" | number[];
export function getTransform(target: SVGElement | HTMLElement, isInit?: boolean) {
const transform = getComputedStyle(target).transform!;
if (!transform || (transform === "none" && !isInit)) {
return "none";
}
return getTransformMatrix(transform);
}
export function getTransformMatrix(transform: string | number[]) {
if (!transform || transform === "none") {
return [1, 0, 0, 1, 0, 0];
}
if (isObject(transform)) {
return transform;
}
const value = splitBracket(transform).value!;
return value.split(/s*,\s*/g).map(v => parseFloat(v));
}
export function getAbsoluteMatrix(matrix: number[], n: number, origin: number[]) {
return multiplies(
n,
createOriginMatrix(origin, n),
matrix,
createOriginMatrix(origin.map(a => -a), n),
);
}
export function measureSVGSize(el: SVGElement, unit: string, isHorizontal: boolean) {
if (unit === "%") {
const viewBox = el.ownerSVGElement!.viewBox.baseVal;
return viewBox[isHorizontal ? "width" : "height"] / 100;
}
return 1;
}
export function getBeforeTransformOrigin(el: SVGElement) {
const relativeOrigin = getTransformOrigin(getComputedStyle(el, ":before"));
return relativeOrigin.map((o, i) => {
const { value, unit } = splitUnit(o);
return value * measureSVGSize(el, unit, i === 0);
});
}
export function getTransformOrigin(style: CSSStyleDeclaration) {
const transformOrigin = style.transformOrigin;
return transformOrigin ? transformOrigin.split(" ") : ["0", "0"];
}
export function caculateMatrixStack(
target: SVGElement | HTMLElement,
container: SVGElement | HTMLElement | null,
prevMatrix?: number[],
prevN?: number,
): [number[], number[], number[], number[], string, number[], boolean] {
let el: SVGElement | HTMLElement | null = target;
const matrixes: number[][] = [];
const isContainer: boolean = !!prevMatrix || target === container;
const isSVGGraphicElement = el.tagName.toLowerCase() !== "svg" && "ownerSVGElement" in el;
let is3d = false;
let n = 3;
let transformOrigin!: number[];
let targetMatrix!: number[];
let style: CSSStyleDeclaration = getComputedStyle(el);
while (el && (isContainer || el !== container)) {
const tagName = el.tagName.toLowerCase();
const position = style.position;
const isFixed = position === "fixed";
let matrix: number[] = convertCSStoMatrix(getTransformMatrix(style.transform!));
if (!is3d && matrix.length === 16) {
is3d = true;
n = 4;
const matrixesLength = matrixes.length;
for (let i = 0; i < matrixesLength; ++i) {
matrixes[i] = convertDimension(matrixes[i], 3, 4);
}
}
if (is3d && matrix.length === 9) {
matrix = convertDimension(matrix, 3, 4);
}
let offsetLeft = (el as any).offsetLeft;
let offsetTop = (el as any).offsetTop;
if (isFixed) {
const containerRect = (container || document.documentElement).getBoundingClientRect();
offsetLeft -= containerRect.left;
offsetTop -= containerRect.top;
}
// svg
const isSVG = isUndefined(offsetLeft);
let hasNotOffset = isSVG;
let origin: number[];
// inner svg element
if (hasNotOffset && tagName !== "svg") {
origin = isWebkit
? getBeforeTransformOrigin(el as SVGElement)
: getTransformOrigin(style).map(pos => parseFloat(pos));
hasNotOffset = false;
if (tagName === "g") {
offsetLeft = 0;
offsetTop = 0;
} else {
[
offsetLeft, offsetTop, origin[0], origin[1],
] = getSVGGraphicsOffset(el as SVGGraphicsElement, origin);
}
} else {
origin = getTransformOrigin(style).map(pos => parseFloat(pos));
}
if (tagName === "svg" && targetMatrix) {
matrixes.push(
getSVGMatrix(el as SVGSVGElement, n),
createIdentityMatrix(n),
);
}
const parentElement: HTMLElement = el.parentElement!;
if (isWebkit && !hasNotOffset && !isSVG) {
const offsetParent = (el as HTMLElement).offsetParent as HTMLElement;
if (offsetParent && offsetParent !== parentElement) {
offsetLeft -= parentElement.offsetLeft;
offsetTop -= parentElement.offsetTop;
}
}
matrixes.push(
getAbsoluteMatrix(matrix, n, origin),
createOriginMatrix([
hasNotOffset ? el : offsetLeft,
hasNotOffset ? origin : offsetTop,
], n),
);
if (!targetMatrix) {
targetMatrix = matrix;
}
if (!transformOrigin) {
transformOrigin = origin;
}
if (isContainer || isFixed) {
break;
}
el = parentElement;
if (isSVG) {
continue;
}
while (el) {
style = getComputedStyle(el);
const {
position: nextPosition,
transform: nextTransform,
} = style;
if (
nextPosition !== "static"
|| (nextTransform && nextTransform !== "none")
) {
break;
}
el = el.parentElement;
}
if (el) {
style = getComputedStyle(el);
}
}
let mat = prevMatrix ? convertDimension(prevMatrix, prevN!, n) : createIdentityMatrix(n);
let beforeMatrix = prevMatrix ? convertDimension(prevMatrix, prevN!, n) : createIdentityMatrix(n);
let offsetMatrix = createIdentityMatrix(n);
const length = matrixes.length;
matrixes.reverse();
matrixes.forEach((matrix, i) => {
if (length - 2 === i) {
beforeMatrix = mat.slice();
}
if (length - 1 === i) {
offsetMatrix = mat.slice();
}
if (isObject(matrix[n - 1])) {
[matrix[n - 1], matrix[2 * n - 1]] =
getSVGOffset(
matrix[n - 1] as any,
container,
n,
matrix[2 * n - 1] as any,
mat,
matrixes[i + 1],
);
}
mat = multiply(
mat,
matrix,
n,
);
});
const isMatrix3d = !isSVGGraphicElement && is3d;
const transform = `${isMatrix3d ? "matrix3d" : "matrix"}(${
convertMatrixtoCSS(isSVGGraphicElement && targetMatrix.length === 16
? convertDimension(targetMatrix, 4, 3) : targetMatrix)
})`;
return [
beforeMatrix, offsetMatrix, mat, targetMatrix, transform, transformOrigin, is3d,
];
}
export function getSVGMatrix(
el: SVGSVGElement,
n: number,
) {
const clientWidth = el.clientWidth;
const clientHeight = el.clientHeight;
const viewBox = (el as SVGSVGElement).viewBox.baseVal;
const viewBoxWidth = viewBox.width || clientWidth;
const viewBoxHeight = viewBox.height || clientHeight;
const scaleX = clientWidth / viewBoxWidth;
const scaleY = clientHeight / viewBoxHeight;
const preserveAspectRatio = (el as SVGSVGElement).preserveAspectRatio.baseVal;
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
const align = preserveAspectRatio.align;
// 1 : meet 2: slice
const meetOrSlice = preserveAspectRatio.meetOrSlice;
const svgOrigin = [0, 0];
const scale = [scaleX, scaleY];
const translate = [0, 0];
if (align !== 1) {
const xAlign = (align - 2) % 3;
const yAlign = Math.floor((align - 2) / 3);
svgOrigin[0] = viewBoxWidth * xAlign / 2;
svgOrigin[1] = viewBoxHeight * yAlign / 2;
const scaleDimension = meetOrSlice === 2 ? Math.max(scaleY, scaleX) : Math.min(scaleX, scaleY);
scale[0] = scaleDimension;
scale[1] = scaleDimension;
translate[0] = (clientWidth - viewBoxWidth) / 2 * xAlign;
translate[1] = (clientHeight - viewBoxHeight) / 2 * yAlign;
}
const scaleMatrix = createScaleMatrix(scale, n);
[
scaleMatrix[n - 1],
scaleMatrix[2 * n - 1],
] = translate;
return getAbsoluteMatrix(
scaleMatrix,
n,
svgOrigin,
);
}
export function getSVGGraphicsOffset(
el: SVGGraphicsElement,
origin: number[],
) {
if (!(el as SVGGraphicsElement).getBBox) {
return [0, 0];
}
const bbox = (el as SVGGraphicsElement).getBBox();
const svgElement = el.ownerSVGElement!;
const viewBox = svgElement.viewBox.baseVal;
const left = bbox.x - viewBox.x;
const top = bbox.y - viewBox.y;
return [
left,
top,
origin[0] - left,
origin[1] - top,
];
}
export function caculatePosition(matrix: number[], pos: number[], n: number) {
return caculate(matrix, convertPositionMatrix(pos, n), n);
}
export function caculatePoses(matrix: number[], width: number, height: number, n: number) {
const pos1 = caculatePosition(matrix, [0, 0], n);
const pos2 = caculatePosition(matrix, [width, 0], n);
const pos3 = caculatePosition(matrix, [0, height], n);
const pos4 = caculatePosition(matrix, [width, height], n);
return [pos1, pos2, pos3, pos4];
}
export function getRect(poses: number[][]) {
const posesX = poses.map(pos => pos[0]);
const posesY = poses.map(pos => pos[1]);
const left = Math.min(...posesX);
const top = Math.min(...posesY);
const right = Math.max(...posesX);
const bottom = Math.max(...posesY);
const rectWidth = right - left;
const rectHeight = bottom - top;
return {
left, top,
right, bottom,
width: rectWidth,
height: rectHeight,
};
}
export function caculateRect(matrix: number[], width: number, height: number, n: number) {
const poses = caculatePoses(matrix, width, height, n);
return getRect(poses);
}
export function getSVGOffset(
el: SVGElement,
container: HTMLElement | SVGElement | null,
n: number, origin: number[], beforeMatrix: number[], absoluteMatrix: number[]) {
const [width, height] = getSize(el);
const containerRect = (container || document.documentElement).getBoundingClientRect();
const rect = el.getBoundingClientRect();
const rectLeft = rect.left - containerRect.left;
const rectTop = rect.top - containerRect.top;
const rectWidth = rect.width;
const rectHeight = rect.height;
const mat = multiplies(
n,
beforeMatrix,
absoluteMatrix,
);
const {
left: prevLeft,
top: prevTop,
width: prevWidth,
height: prevHeight,
} = caculateRect(mat, width, height, n);
const posOrigin = caculatePosition(mat, origin, n);
const prevOrigin = minus(posOrigin, [prevLeft, prevTop]);
const rectOrigin = [
rectLeft + prevOrigin[0] * rectWidth / prevWidth,
rectTop + prevOrigin[1] * rectHeight / prevHeight,
];
const offset = [0, 0];
let count = 0;
while (++count < 10) {
const inverseBeforeMatrix = invert(beforeMatrix, n);
[offset[0], offset[1]] = minus(
caculatePosition(inverseBeforeMatrix, rectOrigin, n),
caculatePosition(inverseBeforeMatrix, posOrigin, n),
);
const mat2 = multiplies(
n,
beforeMatrix,
createOriginMatrix(offset, n),
absoluteMatrix,
);
const {
left: nextLeft,
top: nextTop,
} = caculateRect(mat2, width, height, n);
const distLeft = nextLeft - rectLeft;
const distTop = nextTop - rectTop;
if (Math.abs(distLeft) < 2 && Math.abs(distTop) < 2) {
break;
}
rectOrigin[0] -= distLeft;
rectOrigin[1] -= distTop;
}
return offset.map(p => Math.round(p));
}
export function caculateMoveablePosition(matrix: number[], origin: number[], width: number, height: number): [
number[],
number[],
number[],
number[],
number[],
number[],
1 | -1,
] {
const is3d = matrix.length === 16;
const n = is3d ? 4 : 3;
let [
[x1, y1],
[x2, y2],
[x3, y3],
[x4, y4],
] = caculatePoses(matrix, width, height, n);
let [originX, originY] = caculatePosition(matrix, origin, n);
const left = Math.min(x1, x2, x3, x4);
const top = Math.min(y1, y2, y3, y4);
const right = Math.max(x1, x2, x3, x4);
const bottom = Math.max(y1, y2, y3, y4);
x1 = (x1 - left) || 0;
x2 = (x2 - left) || 0;
x3 = (x3 - left) || 0;
x4 = (x4 - left) || 0;
y1 = (y1 - top) || 0;
y2 = (y2 - top) || 0;
y3 = (y3 - top) || 0;
y4 = (y4 - top) || 0;
originX = (originX - left) || 0;
originY = (originY - top) || 0;
const center = [
(x1 + x2 + x3 + x4) / 4,
(y1 + y2 + y3 + y4) / 4,
];
const pos1Rad = getRad(center, [x1, y1]);
const pos2Rad = getRad(center, [x2, y2]);
const direction =
(pos1Rad < pos2Rad && pos2Rad - pos1Rad < Math.PI) || (pos1Rad > pos2Rad && pos2Rad - pos1Rad < -Math.PI)
? 1 : -1;
return [
[left, top, right, bottom],
[originX, originY],
[x1, y1],
[x2, y2],
[x3, y3],
[x4, y4],
direction,
];
}
export function getLineStyle(pos1: number[], pos2: number[]) {
const distX = pos2[0] - pos1[0];
const distY = pos2[1] - pos1[1];
const width = Math.sqrt(distX * distX + distY * distY);
const rad = getRad(pos1, pos2);
return {
transform: `translate(${pos1[0]}px, ${pos1[1]}px) rotate(${rad}rad)`,
width: `${width}px`,
};
}
export function getControlTransform(...poses: number[][]) {
const length = poses.length;
const x = poses.reduce((prev, pos) => prev + pos[0], 0) / length;
const y = poses.reduce((prev, pos) => prev + pos[1], 0) / length;
return {
transform: `translate(${x}px, ${y}px)`,
};
}
export function getSize(
target: SVGElement | HTMLElement,
style: CSSStyleDeclaration = getComputedStyle(target),
isOffset?: boolean,
isBoxSizing: boolean = isOffset || style.boxSizing === "border-box",
) {
let width = (target as HTMLElement).offsetWidth;
let height = (target as HTMLElement).offsetHeight;
const hasOffset = !isUndefined(width);
if ((isOffset || isBoxSizing) && hasOffset) {
return [width, height];
}
width = target.clientWidth;
height = target.clientHeight;
if (!hasOffset && !width && !height) {
const bbox = (target as SVGGraphicsElement).getBBox();
return [bbox.width, bbox.height];
}
if (isOffset || isBoxSizing) {
const borderLeft = parseFloat(style.borderLeftWidth!) || 0;
const borderRight = parseFloat(style.borderRightWidth!) || 0;
const borderTop = parseFloat(style.borderTopWidth!) || 0;
const borderBottom = parseFloat(style.borderBottomWidth!) || 0;
return [
width + borderLeft + borderRight,
height + borderTop + borderBottom,
];
} else {
const paddingLeft = parseFloat(style.paddingLeft!) || 0;
const paddingRight = parseFloat(style.paddingRight!) || 0;
const paddingTop = parseFloat(style.paddingTop!) || 0;
const paddingBottom = parseFloat(style.paddingBottom!) || 0;
return [
width - paddingLeft - paddingRight,
height - paddingTop - paddingBottom,
];
}
}
export function getTargetInfo(
target?: HTMLElement | SVGElement,
container?: HTMLElement | SVGElement,
state?: Partial<MoveableManagerState> | false | undefined,
): Partial<MoveableManagerState> {
let left = 0;
let top = 0;
let right = 0;
let bottom = 0;
let origin = [0, 0];
let pos1 = [0, 0];
let pos2 = [0, 0];
let pos3 = [0, 0];
let pos4 = [0, 0];
let offsetMatrix = createIdentityMatrix3();
let beforeMatrix = createIdentityMatrix3();
let matrix = createIdentityMatrix3();
let targetMatrix = createIdentityMatrix3();
let width = 0;
let height = 0;
let transformOrigin = [0, 0];
let direction: 1 | -1 = 1;
let beforeDirection: 1 | -1 = 1;
let is3d = false;
let targetTransform = "";
let beforeOrigin = [0, 0];
const prevMatrix = state ? state.beforeMatrix : undefined;
const prevN = state ? (state.is3d ? 4 : 3) : undefined;
if (target) {
if (state) {
width = state.width!;
height = state.height!;
} else {
const style = getComputedStyle(target);
width = (target as HTMLElement).offsetWidth;
height = (target as HTMLElement).offsetHeight;
if (isUndefined(width)) {
[width, height] = getSize(target, style, true);
}
}
[
beforeMatrix, offsetMatrix, matrix,
targetMatrix,
targetTransform, transformOrigin, is3d,
] = caculateMatrixStack(target, container!, prevMatrix, prevN);
[
[left, top, right, bottom],
origin,
pos1,
pos2,
pos3,
pos4,
direction,
] = caculateMoveablePosition(matrix, transformOrigin, width, height);
const n = is3d ? 4 : 3;
let beforePos = [0, 0];
[
beforePos, beforeOrigin, , , , , beforeDirection,
] = caculateMoveablePosition(offsetMatrix, plus(transformOrigin, getOrigin(targetMatrix, n)), width, height);
beforeOrigin = [
beforeOrigin[0] + beforePos[0] - left,
beforeOrigin[1] + beforePos[1] - top,
];
}
return {
beforeDirection,
direction,
target,
left,
top,
right,
bottom,
pos1,
pos2,
pos3,
pos4,
width,
height,
beforeMatrix,
matrix,
targetTransform,
offsetMatrix,
targetMatrix,
is3d,
beforeOrigin,
origin,
transformOrigin,
};
}
export function getDirection(target: SVGElement | HTMLElement) {
if (!target) {
return;
}
const direciton = target.getAttribute("data-direction")!;
if (!direciton) {
return;
}
const dir = [0, 0];
(direciton.indexOf("w") > -1) && (dir[0] = -1);
(direciton.indexOf("e") > -1) && (dir[0] = 1);
(direciton.indexOf("n") > -1) && (dir[1] = -1);
(direciton.indexOf("s") > -1) && (dir[1] = 1);
return dir;
}
export function getAbsolutePoses(poses: number[][], dist: number[]) {
return [
plus(dist, poses[0]),
plus(dist, poses[1]),
plus(dist, poses[2]),
plus(dist, poses[3]),
];
}
export function getAbsolutePosesByState({
left,
top,
pos1,
pos2,
pos3,
pos4,
}: {
left: number,
top: number,
pos1: number[],
pos2: number[],
pos3: number[],
pos4: number[],
}) {
return getAbsolutePoses([pos1, pos2, pos3, pos4], [left, top]);
}
export function throttle(num: number, unit: number) {
if (!unit) {
return num;
}
return Math.round(num / unit) * unit;
}
export function throttleArray(nums: number[], unit: number) {
nums.forEach((_, i) => {
nums[i] = throttle(nums[i], unit);
});
return nums;
}
export function unset(self: any, name: string) {
if (self[name]) {
self[name].unset();
self[name] = null;
}
}
export function getOrientationDirection(pos: number[], pos1: number[], pos2: number[]) {
return (pos[0] - pos1[0]) * (pos2[1] - pos1[1]) - (pos[1] - pos1[1]) * (pos2[0] - pos1[0]);
}
export function isInside(pos: number[], pos1: number[], pos2: number[], pos3: number[], pos4: number[]) {
const k1 = getOrientationDirection(pos, pos1, pos2);
const k2 = getOrientationDirection(pos, pos2, pos4);
const k3 = getOrientationDirection(pos, pos4, pos1);
const k4 = getOrientationDirection(pos, pos2, pos4);
const k5 = getOrientationDirection(pos, pos4, pos3);
const k6 = getOrientationDirection(pos, pos3, pos2);
const signs1 = [k1, k2, k3];
const signs2 = [k4, k5, k6];
if (
signs1.every(sign => sign >= 0)
|| signs1.every(sign => sign <= 0)
|| signs2.every(sign => sign >= 0)
|| signs2.every(sign => sign <= 0)
) {
return true;
}
return false;
}
export function triggerEvent<T extends IObject<any>, U extends keyof T & string>(
moveable: MoveableManager<T>,
name: U,
e: T[U] extends ((e: infer P) => any) | undefined ? P : {},
): any {
return moveable.triggerEvent(name, e);
}
export function getComputedStyle(el: HTMLElement | SVGElement, pseudoElt?: string | null) {
return window.getComputedStyle(el, pseudoElt);
}