react-moveable
Version:
A React Component that create Moveable, Draggable, Resizable, Scalable, Rotatable, Warpable, Pinchable, Groupable.
347 lines (296 loc) • 11.4 kB
text/typescript
import { Able, MoveableManagerInterface, MoveableGroupInterface } from "../types";
import { getWindow, hasClass, IObject } from "@daybrush/utils";
import { convertDragDist, defaultSync, getRefTarget } from "../utils";
import Gesto, { GestoOptions } from "gesto";
import BeforeRenderable from "../ables/BeforeRenderable";
import Renderable from "../ables/Renderable";
export function triggerAble(
moveable: MoveableManagerInterface,
moveableAbles: Able[],
eventOperations: string[],
eventAffix: string,
eventType: any,
e: any,
requestInstant?: boolean,
) {
// pre setting
e.clientDistX = e.distX;
e.clientDistY = e.distY;
const isStart = eventType === "Start";
const isEnd = eventType === "End";
const isAfter = eventType === "After";
const target = moveable.state.target;
const isRequest = e.isRequest;
const isControl = eventAffix.indexOf("Control") > -1;
if (
!target
|| (isStart && isControl && !isRequest && moveable.areaElement === e.inputEvent.target)
) {
return false;
}
const ables: Able[] = [...moveableAbles];
if (isRequest) {
const requestAble = e.requestAble;
if (!ables.some(able => able.name === requestAble)) {
ables.push(...moveable.props.ables!.filter(able => able.name === requestAble));
}
}
if (!ables.length || ables.every(able => able.dragRelation)) {
return false;
}
// "drag" "Control" "After"
const inputEvent = e.inputEvent;
let inputTarget: Element;
if (isEnd && inputEvent) {
inputTarget = document.elementFromPoint(e.clientX, e.clientY) || inputEvent.target;
}
let isDragStop = false;
const stop = () => {
isDragStop = true;
e.stop?.();
};
const isFirstStart = isStart && (
!moveable.targetGesto || !moveable.controlGesto
|| (!moveable.targetGesto.isFlag() || !moveable.controlGesto.isFlag())
);
if (isFirstStart) {
moveable.updateRect(eventType, true, false);
}
// trigger ables
const datas = e.datas;
const gestoType = isControl ? "controlGesto" : "targetGesto";
const prevGesto = moveable[gestoType];
const trigger = (able: any, eventName: string, conditionName?: string) => {
if (!(eventName in able) || prevGesto !== moveable[gestoType]) {
return false;
}
const ableName = able.name;
const nextDatas = datas[ableName] || (datas[ableName] = {});
if (isStart) {
nextDatas.isEventStart = !conditionName
|| !able[conditionName] || able[conditionName](moveable, e);
}
if (!nextDatas.isEventStart) {
return false;
}
const result = able[eventName](moveable, {
...e,
stop,
datas: nextDatas,
originalDatas: datas,
inputTarget,
});
(moveable as any)._emitter.off();
if (isStart && result === false) {
nextDatas.isEventStart = false;
}
return result;
};
// unset ables for first drag start
if (isFirstStart) {
ables.forEach(able => {
able.unset && able.unset(moveable);
});
}
// BeforeRenderable
trigger(BeforeRenderable, `drag${eventAffix}${eventType}`);
let forceEndedCount = 0;
let updatedCount = 0;
eventOperations.forEach(eventOperation => {
if (isDragStop) {
return false;
}
const eventName = `${eventOperation}${eventAffix}${eventType}`;
const conditionName = `${eventOperation}${eventAffix}Condition`;
if (eventType === "" && !isRequest) {
// Convert distX, distY
convertDragDist(moveable.state, e);
}
// const isGroup = eventAffix.indexOf("Group") > -1;
let eventAbles: Able[] = ables.filter((able: any) => able[eventName]);
eventAbles = eventAbles.filter((able, i) => {
return able.name && eventAbles.indexOf(able) === i;
});
const results = eventAbles.filter(able => trigger(able, eventName, conditionName));
const isUpdate = results.length;
// end ables
if (isDragStop) {
++forceEndedCount;
}
if (isUpdate) {
++updatedCount;
}
if (!isDragStop && isStart && eventAbles.length && !isUpdate) {
forceEndedCount += eventAbles.filter(able => {
const ableName = able.name;
const nextDatas = datas[ableName];
if (nextDatas.isEventStart) {
if (able.dragRelation === "strong") {
return false;
}
// stop drag
return true;
}
// pre stop drag
return false;
}).length ? 1 : 0;
}
});
if (!isAfter || updatedCount) {
trigger(Renderable, `drag${eventAffix}${eventType}`);
}
// stop gesto condition
const isForceEnd = prevGesto !== moveable[gestoType] || forceEndedCount === eventOperations.length;
if (isEnd || isDragStop || isForceEnd) {
moveable.state.gestos = {};
if ((moveable as MoveableGroupInterface).moveables) {
(moveable as MoveableGroupInterface).moveables.forEach(childMoveable => {
childMoveable.state.gestos = {};
});
}
ables.forEach(able => {
able.unset && able.unset(moveable);
});
}
if (isStart && !isForceEnd && !isRequest && updatedCount && moveable.props.preventDefault) {
e?.preventDefault();
}
if (moveable.isUnmounted || isForceEnd) {
return false;
}
if ((!isStart && updatedCount && !requestInstant) || isEnd) {
const flushSync = moveable.props.flushSync || defaultSync;
flushSync(() => {
moveable.updateRect(isEnd ? eventType : "", true, false);
moveable.forceUpdate();
});
}
if (!isStart && !isEnd && !isAfter && updatedCount && !requestInstant) {
triggerAble(moveable, moveableAbles, eventOperations, eventAffix, eventType + "After", e);
}
return true;
}
export function checkMoveableTarget(moveable: MoveableManagerInterface, isControl?: boolean) {
return (e: { inputEvent: Event }, target: EventTarget | null = e.inputEvent.target) => {
const eventTarget = target as Element;
const areaElement = moveable.areaElement;
const dragTargetElement = (moveable as any)._dragTarget;
if (!dragTargetElement || (!isControl && moveable.controlGesto?.isFlag())) {
return false;
}
return eventTarget === dragTargetElement
|| dragTargetElement.contains(eventTarget)
|| eventTarget === areaElement
|| (!moveable.isMoveableElement(eventTarget) && !moveable.controlBox.contains(eventTarget))
|| hasClass(eventTarget, "moveable-area")
|| hasClass(eventTarget, "moveable-padding")
|| hasClass(eventTarget, "moveable-edgeDraggable");
};
}
export function getTargetAbleGesto(
moveable: MoveableManagerInterface,
moveableTarget: HTMLElement | SVGElement,
eventAffix: string,
) {
const controlBox = moveable.controlBox;
const targets: Array<HTMLElement | SVGElement> = [];
const props = moveable.props;
const dragArea = props.dragArea;
const target = moveable.state.target;
const dragTarget = props.dragTarget;
targets.push(controlBox);
if (!dragArea || dragTarget) {
targets.push(moveableTarget);
}
if (!dragArea && dragTarget && target && moveableTarget !== target && props.dragTargetSelf) {
targets.push(target);
}
const checkTarget = checkMoveableTarget(moveable);
return getAbleGesto(moveable, targets, "targetAbles", eventAffix, {
dragStart: checkTarget,
pinchStart: checkTarget,
});
}
export function getControlAbleGesto(
moveable: MoveableManagerInterface,
eventAffix: string,
) {
const controlBox = moveable.controlBox;
const targets: Array<HTMLElement | SVGElement> = [];
targets.push(controlBox);
const checkTarget = checkMoveableTarget(moveable, true);
const checkControlTarget = (e: any, target: EventTarget | null = e.inputEvent.target) => {
if (target === controlBox) {
return true;
}
const result = checkTarget(e, target);
return !result;
};
return getAbleGesto(moveable, targets, "controlAbles", eventAffix, {
dragStart: checkControlTarget,
pinchStart: checkControlTarget,
});
}
export function getAbleGesto(
moveable: MoveableManagerInterface,
target: HTMLElement | SVGElement | Array<HTMLElement | SVGElement>,
ableType: string,
eventAffix: string,
conditionFunctions: IObject<any> = {},
) {
const isTargetAbles = ableType === "targetAbles";
const {
pinchOutside,
pinchThreshold,
preventClickEventOnDrag,
preventClickDefault,
checkInput,
dragFocusedInput,
preventDefault = true,
preventRightClick = true,
preventWheelClick = true,
dragContainer: dragContaienrOption,
} = moveable.props;
const dragContainer = getRefTarget(dragContaienrOption, true);
const options: GestoOptions = {
preventDefault,
preventRightClick,
preventWheelClick,
container: dragContainer || getWindow(moveable.getControlBoxElement()),
pinchThreshold,
pinchOutside,
preventClickEventOnDrag: isTargetAbles ? preventClickEventOnDrag : false,
preventClickEventOnDragStart: isTargetAbles ? preventClickDefault : false,
preventClickEventByCondition: isTargetAbles ? null : (e: MouseEvent) => {
return moveable.controlBox.contains(e.target as Element);
},
checkInput: isTargetAbles ? checkInput : false,
dragFocusedInput,
};
const gesto = new Gesto(target!, options);
const isControl = eventAffix === "Control";
["drag", "pinch"].forEach(eventOperation => {
["Start", "", "End"].forEach(eventType => {
gesto.on(`${eventOperation}${eventType}` as any, e => {
const eventName = e.eventType;
const isPinchScheduled = eventOperation === "drag" && e.isPinch;
if (conditionFunctions[eventName] && !conditionFunctions[eventName](e)) {
e.stop();
return;
}
if (isPinchScheduled) {
return;
}
const eventOperations = eventOperation === "drag" ? [eventOperation] : ["drag", eventOperation];
const moveableAbles: Able[] = [...(moveable as any)[ableType]];
const result = triggerAble(moveable, moveableAbles, eventOperations, eventAffix, eventType, e);
if (!result) {
e.stop();
} else if (moveable.props.stopPropagation || (eventType === "Start" && isControl)) {
e?.inputEvent?.stopPropagation();
}
});
});
});
return gesto;
}