packages/react-moveable/src/ables/Draggable.tsx

import {
    setDragStart, getBeforeDragDist, getTransformDist,
    convertTransformFormat, resolveTransformEvent, fillTransformStartEvent,
    setDefaultTransformIndex, fillOriginalTransform,
} from "../gesto/GestoUtils";
import {
    triggerEvent, fillParams,
    getDistSize, prefix,
    fillEndParams,
    fillCSSObject,
} from "../utils";
import { minus, plus } from "@scena/matrix";
import {
    DraggableProps, OnDrag, OnDragGroup,
    OnDragGroupStart, OnDragStart, OnDragEnd, DraggableState,
    Renderer, OnDragGroupEnd, MoveableManagerInterface, MoveableGroupInterface,
} from "../types";
import { triggerChildGesto } from "../groupUtils";
import { startCheckSnapDrag } from "./Snappable";
import { getRad, throttle, throttleArray } from "@daybrush/utils";
import { checkSnapBoundsDrag } from "./snappable/snapBounds";
import { TINY_NUM } from "../consts";

/**
 * @namespace Draggable
 * @memberof Moveable
 * @description Draggable refers to the ability to drag and move targets.
 */
export default {
    name: "draggable",
    props: [
        "draggable",
        "throttleDrag",
        "throttleDragRotate",
        "hideThrottleDragRotateLine",
        "startDragRotate",
        "edgeDraggable",
    ] as const,
    events: [
        "dragStart",
        "drag",
        "dragEnd",
        "dragGroupStart",
        "dragGroup",
        "dragGroupEnd",
    ] as const,
    requestStyle(): string[] {
        return ["left", "top", "right", "bottom"];
    },
    requestChildStyle(): string[] {
        return ["left", "top", "right", "bottom"];
    },
    render(
        moveable: MoveableManagerInterface<DraggableProps, DraggableState>,
        React: Renderer,
    ): any[] {
        const { hideThrottleDragRotateLine, throttleDragRotate, zoom } = moveable.props;
        const { dragInfo, beforeOrigin } = moveable.getState();

        if (hideThrottleDragRotateLine || !throttleDragRotate || !dragInfo) {
            return [];
        }
        const dist = dragInfo.dist;

        if (!dist[0] && !dist[1]) {
            return [];
        }

        const width = getDistSize(dist);
        const rad = getRad(dist, [0, 0]);

        return [<div className={prefix(
            "line",
            "horizontal",
            "dragline",
            "dashed",
        )} key={`dragRotateGuideline`} style={{
            width: `${width}px`,
            transform: `translate(${beforeOrigin[0]}px, ${beforeOrigin[1]}px) rotate(${rad}rad) scaleY(${zoom})`,
        }} />];
    },
    dragStart(
        moveable: MoveableManagerInterface<DraggableProps, any>,
        e: any,
    ) {
        const { datas, parentEvent, parentGesto } = e;
        const state = moveable.state;
        const {
            gestos,
            style,
        } = state;

        if (gestos.draggable) {
            return false;
        }
        gestos.draggable = parentGesto || moveable.targetGesto;

        datas.datas = {};
        datas.left = parseFloat(style.left || "") || 0;
        datas.top = parseFloat(style.top || "") || 0;
        datas.bottom = parseFloat(style.bottom || "") || 0;
        datas.right = parseFloat(style.right || "") || 0;
        datas.startValue = [0, 0];

        setDragStart(moveable, e);
        setDefaultTransformIndex(moveable, e, "translate");
        startCheckSnapDrag(moveable, datas);

        datas.prevDist = [0, 0];
        datas.prevBeforeDist = [0, 0];
        datas.isDrag = false;
        datas.deltaOffset = [0, 0];

        const params = fillParams<OnDragStart>(moveable, e, {
            set: (translate: number[]) => {
                datas.startValue = translate;
            },
            ...fillTransformStartEvent(moveable, e),
        });
        const result = parentEvent || triggerEvent(moveable, "onDragStart", params);

        if (result !== false) {
            datas.isDrag = true;
            moveable.state.dragInfo = {
                startRect: moveable.getRect(),
                dist: [0, 0],
            };
        } else {
            gestos.draggable = null;
            datas.isPinch = false;
        }
        return datas.isDrag ? params : false;
    },
    drag(
        moveable: MoveableManagerInterface<DraggableProps, any>,
        e: any,
    ): OnDrag | undefined {
        if (!e) {
            return;
        }
        resolveTransformEvent(moveable, e, "translate");

        const {
            datas, parentEvent,
            parentFlag, isPinch, deltaOffset,
            useSnap,
            isRequest,
            isGroup,
            parentThrottleDrag,
        } = e;
        let { distX, distY } = e;
        const { isDrag, prevDist, prevBeforeDist, startValue } = datas;

        if (!isDrag) {
            return;
        }

        if (deltaOffset) {
            distX += deltaOffset[0];
            distY += deltaOffset[1];
        }
        const props = moveable.props;

        const parentMoveable = props.parentMoveable;
        const throttleDrag = isGroup ? 0 : (props.throttleDrag || parentThrottleDrag || 0);
        const throttleDragRotate = parentEvent ? 0 : (props.throttleDragRotate || 0);

        let dragRotateRad = 0;
        let isVerticalSnap = false;
        let isVerticalBound = false;
        let isHorizontalSnap = false;
        let isHorizontalBound = false;

        if (!parentEvent && throttleDragRotate > 0 && (distX || distY)) {
            const startDragRotate = props.startDragRotate || 0;
            const deg
                = throttle(startDragRotate + getRad([0, 0], [distX, distY]) * 180 / Math.PI, throttleDragRotate)
                - startDragRotate;
            const ry = distY * Math.abs(Math.cos((deg - 90) / 180 * Math.PI));
            const rx = distX * Math.abs(Math.cos(deg / 180 * Math.PI));
            const r = getDistSize([rx, ry]);
            dragRotateRad = deg * Math.PI / 180;

            distX = r * Math.cos(dragRotateRad);
            distY = r * Math.sin(dragRotateRad);
        }

        if (!isPinch && !parentEvent && !parentFlag) {
            const [verticalInfo, horizontalInfo] = checkSnapBoundsDrag(
                moveable, distX, distY,
                throttleDragRotate,
                (!useSnap && isRequest) || deltaOffset,
                datas,
            );
            isVerticalSnap = verticalInfo.isSnap;
            isVerticalBound = verticalInfo.isBound;
            isHorizontalSnap = horizontalInfo.isSnap;
            isHorizontalBound = horizontalInfo.isBound;

            const verticalOffset = verticalInfo.offset;
            const horizontalOffset = horizontalInfo.offset;

            distX += verticalOffset;
            distY += horizontalOffset;
        }

        const beforeTranslate = plus(getBeforeDragDist({ datas, distX, distY }), startValue);
        const translate = plus(getTransformDist({ datas, distX, distY }), startValue);

        throttleArray(translate, TINY_NUM);
        throttleArray(beforeTranslate, TINY_NUM);

        if (!throttleDragRotate) {
            if (!isVerticalSnap && !isVerticalBound) {
                translate[0] = throttle(translate[0], throttleDrag);
                beforeTranslate[0] = throttle(beforeTranslate[0], throttleDrag);
            }
            if (!isHorizontalSnap && !isHorizontalBound) {
                translate[1] = throttle(translate[1], throttleDrag);
                beforeTranslate[1] = throttle(beforeTranslate[1], throttleDrag);
            }
        }


        const beforeDist = minus(beforeTranslate, startValue);
        const dist = minus(translate, startValue);
        const delta = minus(dist, prevDist);
        const beforeDelta = minus(beforeDist, prevBeforeDist);

        datas.prevDist = dist;
        datas.prevBeforeDist = beforeDist;


        datas.passDelta = delta; //distX - (datas.passDistX || 0);
        // datas.passDeltaY = distY - (datas.passDistY || 0);
        datas.passDist = dist; //distX;
        // datas.passDistY = distY;

        const left = datas.left + beforeDist[0];
        const top = datas.top + beforeDist[1];
        const right = datas.right - beforeDist[0];
        const bottom = datas.bottom - beforeDist[1];
        const nextTransform = convertTransformFormat(datas,
            `translate(${translate[0]}px, ${translate[1]}px)`, `translate(${dist[0]}px, ${dist[1]}px)`);

        fillOriginalTransform(e, nextTransform);

        moveable.state.dragInfo.dist = parentEvent ? [0, 0] : dist;
        if (!parentEvent && !parentMoveable && delta.every(num => !num) && beforeDelta.some(num => !num)) {
            return;
        }

        const {
            width,
            height,
        } = moveable.state;
        const params = fillParams<OnDrag>(moveable, e, {
            transform: nextTransform,
            dist,
            delta,
            translate,
            beforeDist,
            beforeDelta,
            beforeTranslate,
            left,
            top,
            right,
            bottom,
            width,
            height,
            isPinch,
            ...fillCSSObject({
                transform: nextTransform,
            }, e),
        });

        !parentEvent && triggerEvent(moveable, "onDrag", params);
        return params;
    },
    dragAfter(
        moveable: MoveableManagerInterface<DraggableProps, DraggableState>,
        e: any,
    ) {
        const datas = e.datas;
        const {
            deltaOffset,
        } = datas;

        if (deltaOffset[0] || deltaOffset[1]) {
            datas.deltaOffset = [0, 0];
            return this.drag(moveable, {...e, deltaOffset });
        }
        return false;
    },
    dragEnd(
        moveable: MoveableManagerInterface<DraggableProps, DraggableState>,
        e: any,
    ) {
        const { parentEvent, datas } = e;

        moveable.state.dragInfo = null;
        if (!datas.isDrag) {
            return;
        }
        datas.isDrag = false;
        const param = fillEndParams<OnDragEnd>(moveable, e, {});
        !parentEvent && triggerEvent(moveable, "onDragEnd", param);
        return param;
    },
    dragGroupStart(moveable: MoveableGroupInterface<any, any>, e: any) {
        const { datas, clientX, clientY } = e;

        const params = this.dragStart(moveable, e);

        if (!params) {
            return false;
        }
        const {
            childEvents,
            eventParams,
        } = triggerChildGesto(moveable, this, "dragStart", [
            clientX || 0,
            clientY || 0,
        ], e, false, "draggable");

        const nextParams: OnDragGroupStart = {
            ...params,
            targets: moveable.props.targets!,
            events: eventParams,
        };
        const result = triggerEvent(moveable, "onDragGroupStart", nextParams);

        datas.isDrag = result !== false;


        // find data.startValue and based on first child moveable
        const startValue = childEvents[0]?.datas.startValue ?? [0, 0];


        datas.throttleOffset = [startValue[0] % 1, startValue[1] % 1];

        return datas.isDrag ? params : false;
    },
    dragGroup(moveable: MoveableGroupInterface<any, any>, e: any) {
        const { datas } = e;

        if (!datas.isDrag) {
            return;
        }
        const params = this.drag(moveable, {
            ...e,
            parentThrottleDrag: moveable.props.throttleDrag,
        });
        const { passDelta } = e.datas;
        const {
            eventParams,
        } = triggerChildGesto(moveable, this, "drag", passDelta, e, false, "draggable");

        if (!params) {
            return;
        }

        const nextParams: OnDragGroup = {
            targets: moveable.props.targets!,
            events: eventParams,
            ...params,
        };

        triggerEvent(moveable, "onDragGroup", nextParams);
        return nextParams;
    },
    dragGroupEnd(moveable: MoveableGroupInterface<any, any>, e: any) {
        const { isDrag, datas } = e;

        if (!datas.isDrag) {
            return;
        }
        this.dragEnd(moveable, e);
        const {
            eventParams,
        } = triggerChildGesto(moveable, this, "dragEnd", [0, 0], e, false, "draggable");
        triggerEvent(moveable, "onDragGroupEnd", fillEndParams<OnDragGroupEnd>(moveable, e, {
            targets: moveable.props.targets!,
            events: eventParams,
        }));

        return isDrag;
    },
    /**
     * @method Moveable.Draggable#request
     * @param {object} [e] - the draggable's request parameter
     * @param {number} [e.x] - x position
     * @param {number} [e.y] - y position
     * @param {number} [e.deltaX] - X number to move
     * @param {number} [e.deltaY] - Y number to move
     * @return {Moveable.Requester} Moveable Requester
     * @example

     * // Instantly Request (requestStart - request - requestEnd)
     * // Use Relative Value
     * moveable.request("draggable", { deltaX: 10, deltaY: 10 }, true);
     * // Use Absolute Value
     * moveable.request("draggable", { x: 200, y: 100 }, true);
     *
     * // requestStart
     * const requester = moveable.request("draggable");
     *
     * // request
     * // Use Relative Value
     * requester.request({ deltaX: 10, deltaY: 10 });
     * requester.request({ deltaX: 10, deltaY: 10 });
     * requester.request({ deltaX: 10, deltaY: 10 });
     * // Use Absolute Value
     * moveable.request("draggable", { x: 200, y: 100 });
     * moveable.request("draggable", { x: 220, y: 100 });
     * moveable.request("draggable", { x: 240, y: 100 });
     *
     * // requestEnd
     * requester.requestEnd();
     */
    request(moveable: MoveableManagerInterface<any, any>) {
        const datas = {};
        const rect = moveable.getRect();
        let distX = 0;
        let distY = 0;
        let useSnap = false;

        return {
            isControl: false,
            requestStart(e: Record<string, any>) {
                useSnap = e.useSnap;
                return { datas, useSnap };
            },
            request(e: Record<string, any>) {
                if ("x" in e) {
                    distX = e.x - rect.left;
                } else if ("deltaX" in e) {
                    distX += e.deltaX;
                }
                if ("y" in e) {
                    distY = e.y - rect.top;
                } else if ("deltaY" in e) {
                    distY += e.deltaY;
                }

                return { datas, distX, distY, useSnap };
            },
            requestEnd() {
                return { datas, isDrag: true, useSnap };
            },
        };
    },
    unset(moveable: MoveableManagerInterface<any, Record<string, any>>) {
        moveable.state.gestos.draggable = null;
        moveable.state.dragInfo = null;
    },
};

/**
 * Whether or not target can be dragged. (default: false)
 * @name Moveable.Draggable#draggable
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body);
 *
 * moveable.draggable = true;
 */

/**
 * throttle of x, y when drag.
 * @name Moveable.Draggable#throttleDrag
 * @default 0
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body);
 *
 * moveable.throttleDrag = 1;
 */

/**
* throttle of angle of x, y when drag.
* @name Moveable.Draggable#throttleDragRotate
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body);
*
* moveable.throttleDragRotate = 45;
*/

/**
* start angle of throttleDragRotate of x, y when drag.
* @name Moveable.Draggable#startDragRotate
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body);
*
* // 45, 135, 225, 315
* moveable.throttleDragRotate = 90;
* moveable.startDragRotate = 45;
*/

/**
 * When the drag starts, the dragStart event is called.
 * @memberof Moveable.Draggable
 * @event dragStart
 * @param {Moveable.Draggable.OnDragStart} - Parameters for the dragStart event
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, { draggable: true });
 * moveable.on("dragStart", ({ target }) => {
 *     console.log(target);
 * });
 */
/**
 * When dragging, the drag event is called.
 * @memberof Moveable.Draggable
 * @event drag
 * @param {Moveable.Draggable.OnDrag} - Parameters for the drag event
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, { draggable: true });
 * moveable.on("drag", ({ target, transform }) => {
 *     target.style.transform = transform;
 * });
 */
/**
 * When the drag finishes, the dragEnd event is called.
 * @memberof Moveable.Draggable
 * @event dragEnd
 * @param {Moveable.Draggable.OnDragEnd} - Parameters for the dragEnd event
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, { draggable: true });
 * moveable.on("dragEnd", ({ target, isDrag }) => {
 *     console.log(target, isDrag);
 * });
 */

/**
* When the group drag starts, the `dragGroupStart` event is called.
* @memberof Moveable.Draggable
* @event dragGroupStart
* @param {Moveable.Draggable.OnDragGroupStart} - Parameters for the `dragGroupStart` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
*     target: [].slice.call(document.querySelectorAll(".target")),
*     draggable: true
* });
* moveable.on("dragGroupStart", ({ targets }) => {
*     console.log("onDragGroupStart", targets);
* });
*/

/**
* When the group drag, the `dragGroup` event is called.
* @memberof Moveable.Draggable
* @event dragGroup
* @param {Moveable.Draggable.OnDragGroup} - Parameters for the `dragGroup` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
*     target: [].slice.call(document.querySelectorAll(".target")),
*     draggable: true
* });
* moveable.on("dragGroup", ({ targets, events }) => {
*     console.log("onDragGroup", targets);
*     events.forEach(ev => {
*          // drag event
*          console.log("onDrag left, top", ev.left, ev.top);
*          // ev.target!.style.left = `${ev.left}px`;
*          // ev.target!.style.top = `${ev.top}px`;
*          console.log("onDrag translate", ev.dist);
*          ev.target!.style.transform = ev.transform;)
*     });
* });
*/

/**
 * When the group drag finishes, the `dragGroupEnd` event is called.
 * @memberof Moveable.Draggable
 * @event dragGroupEnd
 * @param {Moveable.Draggable.OnDragGroupEnd} - Parameters for the `dragGroupEnd` event
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, {
 *     target: [].slice.call(document.querySelectorAll(".target")),
 *     draggable: true
 * });
 * moveable.on("dragGroupEnd", ({ targets, isDrag }) => {
 *     console.log("onDragGroupEnd", targets, isDrag);
 * });
 */