js 实现模块拖动自由布局

本文介绍了如何使用JavaScript实现模块的拖动自由布局功能。包括拖动后模块的移动逻辑,坐标判断,以及拖动过程中对相邻模块的影响。同时,文章提及了布局信息的存储,以及模块缩放功能的实现。通过封装拖动类,如DragComm、dragArea、dragMenu和zoomArea,来处理拖动、缩放和布局更新。拖动过程中,通过坐标计算预设模块位置,保证布局的正确性和性能。

项目中需要实现在主面板中,多个模块拖动布局功能,同时模块拖出主面板后变成图标展示在面板左侧,并对布局信息进行存储功能

实现思路:首先是思考拖动后相关相交模块的移动方向,这里所用模块自上而下排列,只要上边有足够的空间,则向上排列,其次左右相交的,判断拖动模块和相交模块的中心,根据中心位置,判断拖动模块是放在相交的上边还是下边;

因为频繁获取div位置信息会大大降低页面渲染性能,所以采用坐标进行div位置的判断,可以把面板放置在一个直角坐标系,把面板的宽和高分成若干等分,比如宽度10份,高度10份,这样一个模块的坐标为Grix:0,GridY:0,w:5,h:5 相当于模块在左上角宽高占面板的一半

代码实现上,封装多个拖动类,其中核心类为DragComm,拖动和缩放后引起的模块位置移动均在这里处理,setShadowPos为计算主方法,shadowPos为当前dom的跟随dom,用于预设应该放置的位置,当移动后,根据位移设置shadow的坐标,因为移动产生的像素会频繁触发计算影响性能,所以通过判断当坐标有变动时,才触发setShadowPos计算,主要逻辑是先计算当前操作对象最小放置高度,即根据当前对象上方有交叉的元素获取最小GridY;然后根据移动方向,如果是向下移动,则说明是想要上下交换位置,获取到所有重叠元素后判断元素中心,GridY小于操作元素中心的即为需要向上移动的元素,使用getTopHeight计算出每个的最小GridY后,当前移动元素的最小GridY为向上元素中GridY + h 的最大值;接着判断对其下方元素造成的影响...

dragComm:设置x、y轴分割份数,一个模块的最小尺寸(最小占据份数)GridPos全局位置对象等

dragArea:

        mouseDown:清理定时器(App.mouseUpTimer,拖动模块的保存机制为鼠标up后的5s后调用接口保存,5s内再发起的拖动属于一次拖动)获取父元素及可移动范围元素的尺寸,计算出每份的尺寸prx,pry;设置shadowPos阴影位置信息(dom的跟随阴影dom,用于预设应该放置的位置),创建阴影元素shadowEle,位置信息和阴影dom位置与当前操作元素保持一致

        mouseMove:操作元素跟随鼠标移动,通过鼠标移动位置计算坐标位置(Math.round(x / this.prx);Math.round(y / this.pry)),调用setShadowPos计算阴影的真实位置(确保各模块不相交);判断鼠标是否移出移动范围,是的话说明是要放在外部展示,调用mouseUp结束当前class,父元素中删除该dom对象,GridPos中删除该模块位置信息,展示该模块对应的图标,触发图标mouseDown事件

        mouseup:删除shadowEle,设置操作元素位置为阴影dom位置,设置定时器App.mouseUpTimer(5s后接口保存)

dragMenu:

        mouseDown:计算prx,pry,获取菜单icon列表下右侧展示的icon节点(childrenList)及icon位置高度列表(childrenPosition),创建seat节点,移动排序时用来显示插入位置;设置shadowPos阴影坐标,GridPos中添加该icon对应的模块坐标(mouseUp时若是移入,直接更新坐标,若是排序需要删除该坐标)

        mouseMove:icon拖动按钮只能在模块放置区域和右侧列表内拖动,当判断是在右侧列表内移动时,通过childrenPosition判断插入位置,插入seat,并记录seatIndex;若是在模块区域移动,则移除seat,调用showPlace计算模块要放置的位置信息,然后调用setShadowPos计算出真实要放置的位置

        mouseUp:依然分两种情况,(1)排序,即移出,先从dom中删除该icon,然后插入到seatIndex对应位置,执行putOut从模块区域删除icon对应模块(drag移出触发dragMenu时,模块是在模块区域的,需要删除),GridPos中删除该模块坐标,删除阴影dom;(2)移入,隐藏icon图标,执行putIn将icon对应模块放入模块区域,initPutInEle设置模块放置位置样式,删除阴影dom

zoomArea:

        模块区域内的模块只能使用左下角进行缩放,除了位置会变化,模块宽高也会变化,通过prx、pry计算出坐标信息,依然使用setShadowPos计算真实位置信息

dragComm:

是上边三个类的父类,setShadowPos为计算主方法,传入位置坐标,判断坐标是否有变动,有变动的话进入主计算逻辑(通过转换成坐标,坐标变动触发避免通过像素变更频繁触发影响性能);

getTopheight计算当前操作对象最小放置高度,即根据当前对象上方有交叉的元素获取最小GridY;getCrossList方法获取相交元素列表crossList,crossList为空表示无相交,则直接设置阴影位置坐标,若crossList不为空,则表示有相交,然后根据移动方向directionY,如果是向下移动,则说明是想要向下交换位置,在crossList中找出gridY<= y + h/2(交叉元素中gridY在当前操作元素的中心上方的元素)的元素upList,遍历upList重新计算gridY(忽略处在上方的当前操作元素上移),重新设置传入的y值为移动后的元素中下边距最靠下的位置(y = Math.max(ty + e.h, sy))

根据变更后的y再次更新相交列表crossList,获取需要向下移动的列表downList,当downList.length > 1时,当相交的元素中有上下挨着的,需要删除上下挨着的元素中位于下方的元素(位置修改时,会递归修改下方的元素,避免修改重复和进行不必要的递归);遍历downList,使用changeRelative递归修改紧邻下方的元素位置(downList中分两种情况,一种gridY在同一水平线,一种的是从中空下移,都通过sy + h - d.gridY计算出下移量,中空下移只会在当前层出现),

最后设置shadowEle显示位置,sortElePos方法重新修正各模块显示位置

import App from "@/application/app";
import { LayoutPos, ElePos } from "@/application/app.type";
import { filter } from "lodash";
import { DomUtil } from "./domUtil";
const gap = 8;
const body = document.body || document.documentElement;

export class DragComm extends DomUtil {
    public static GridPos: ElePos[] = [];
    // public static mouseUpTimer: NodeJS.Timeout;
    tx = 12; // x轴分的份数
    ty = 10; // y轴分的份数
    mw = 2; // 最小宽度份数
    mh = 3; // 最小高度份数
    initw = 4; // 初始块的宽度份数
    inith = 5; // 初始块的高度份数
    ppx: number; // x轴每份占的百分比
    ppy: number; // y轴每份占的百分比
    prx: number; // x轴每份的长度
    pry: number; // y轴每份的长度
    source: HTMLElement; // 源容器
    parent: HTMLElement; //父容器
    px = 0;
    py = 0;
    pw = 0;
    ph = 0;
    x = 0;
    y = 0;
    w = 0;
    h = 0;
    directionX = 0;
    directionY = 0;
    shadowEle: HTMLElement; // 影子
    dataSet: string;
    originPos: ElePos; // 初始坐标
    shadowPos: ElePos; // 阴影坐标
    marker: HTMLElement;
    time: number = null;
    dragType: string;
    elObj: {
        [key: string]: HTMLElement;
    };

    constructor(source: HTMLElement, parent: HTMLElement, public app: App) {
        super();
        this.source = source;
        this.parent = parent;
        DragComm.GridPos = App.layoutPos.elePos;

        this.initData();
    }
    initData() {
        // 为空时设置初始化坐标
        if (DragComm.GridPos.length === 0) {
            DragComm.GridPos.push({
                gridX: 0,
                gridY: 0,
                w: this.tx,
                h: this.ty,
                key: "0",
            });
        }
        // 计算x、y每块的大小
        this.ppx = 100 / this.tx;
        this.ppy = 100 / this.ty;
        this.dataSet = this.source.dataset.i || "";
    }
    // 根据坐标初始化位置
    initSize() {
        const gridPos = DragComm.GridPos;
        const children = this.getChildren(this.parent);
        children.forEach(c => {
            const key = c.dataset.i;
            const pos = gridPos.filter(p => p.key === key)[0];
            this.setPosition(c, pos);
        });
    }
    // 移入后初始化位置
    initPutInEle(key: string) {
        const children = this.getChildren(this.parent);
        const c = children.filter(c => c.dataset.i === key)[0];
        this.setPosition(c, this.shadowPos);
        // 设置动画
        this.cardShow(c);
    }
    initMouseDown() {
        this.clearUpTime();
        this.parent.style.userSelect = "none";
        this.source.style.userSelect = "none";
        body.style.userSelect = "none";
        this.source.style.transition = "none";
        this.source.style.zIndex = "10";

        const { x, y, w, h } = this.geOffset(this.source);
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;

        const { x: px, y: py, w: pw, h: ph } = this.getScale(this.parent);
        [this.px, this.py, this.pw, this.ph] = [px, py, pw, ph];
        // 分割的每小块的尺寸
        this.prx = pw / this.tx;
        this.pry = ph / this.ty;

        this.originPos = DragComm.GridPos.filter(d => d.key === this.dataSet)[0];
        this.shadowPos = { ...this.originPos };

        this.shadowEle = this.parent.querySelector(".vq-shadow-follow");
        if (!this.shadowEle) {
            this.shadowEle = this.create("div", "vq-shadow-follow", this.parent);
            this.shadowEle.style.display = "none";
            this.shadowEle.style.left = x + "px";
            this.shadowEle.style.top = y + "px";
            this.shadowEle.style.width = w + "px";
            this.shadowEle.style.height = h + "px";
            this.shadowEle.innerHTML = "<span>松手即可置入面板</span>";
        }
        this.setEleList();

        this.time = new Date().getTime();
        this.marker = document.createElement("div");
        this.marker.style.position = "fixed";
        this.marker.style.top = "0";
        this.marker.style.left = "0";
        this.marker.style.bottom = "0";
        this.marker.style.width = "100%";
        this.marker.style.height = "100%";
        this.marker.style.right = "0";
        this.marker.style.zIndex = "1000";

        body.appendChild(this.marker);
    }
    initMouseUp() {
        this.parent.style.userSelect = "";
        this.source.style.userSelect = "";
        this.source.style.transition = "";
        this.source.style.zIndex = "";
        body.style.userSelect = "";

        if (this.marker) {
            this.remove(this.marker);
        }
        if (this.shadowEle) {
            this.remove(this.shadowEle);
        }
        this.setPosition(this.source, this.shadowPos);
        this.saveLayout();
        // for (let i = 0; i < DragComm.GridPos.length; i++) {
        //     if (DragComm.GridPos[i].key === this.dataSet) {
        //         DragComm.GridPos[i] = { ...this.shadowPos };
        //         return;
        //     }
        // }
    }
    clearUpTime() {
        clearTimeout(App.mouseUpTimer);
    }
    saveLayout() {
        App.layoutPos.elePos = DragComm.GridPos;
        App.setMouseUpTimer();
    }

    // 获取dom节点并保存,方便修改dom样式
    setEleList() {
        this.elObj = {};
        const children = this.getChildren(this.parent);
        children.forEach(c => {
            const key = c.dataset.i;
            if (key) {
                this.elObj[key] = c;
            }
        });
    }
    // 坐标转换位置
    setPosition(ele: HTMLElement, pos?: ElePos) {
        if (!pos) {
            const key = ele.dataset.i;
            pos = DragComm.GridPos.filter(p => p.key === key)[0];
        }
        ele.style.left = pos.gridX * this.ppx + "%";
        ele.style.top = pos.gridY * this.ppy + "%";
        ele.style.width = `calc(${pos.w * this.ppx + "%"} - ${gap}px)`;
        ele.style.height = `calc(${pos.h * this.ppy + "%"} - ${gap}px)`;
    }
    setPositionByWH(ele: HTMLElement, w: number, h: number) {
        ele.style.width = `calc(${w * this.ppx + "%"} - ${gap}px)`;
        ele.style.height = `calc(${h * this.ppy + "%"} - ${gap}px)`;
    }
    setPositionByXY(ele: HTMLElement, x: number, y: number) {
        ele.style.left = x * this.ppx + "%";
        ele.style.top = y * this.ppy + "%";
    }
    setPositionByY(ele: HTMLElement, y: number) {
        ele.style.top = y * this.ppy + "%";
    }

    // 根据菜单图标获取要放置的位置
    showPlace(x: number, y: number) {
        this.shadowEle.style.display = "";
        const left = x - this.px - 30;
        const top = y - this.py - 30;
        let sx = Math.round(left / this.prx);
        let sy = Math.round(top / this.pry);
        sx = sx < 0 ? 0 : sx;
        sx = sx > this.tx - this.initw ? this.tx - this.initw : sx;
        sy = sy < 0 ? 0 : sy;
        this.setShadowPos(sx, sy);
    }
    removeShadowEle() {
        this.remove(this.shadowEle);
    }
    getCrossList(x: number, y: number, w: number, h: number, ignor: string) {
        const crossList: ElePos[] = [];
        DragComm.GridPos.forEach(d => {
            if (d.key !== ignor) {
                const lx = x - (d.gridX + d.w);
                const rx = d.gridX - (x + w);
                const ty = y - (d.gridY + d.h);
                const by = d.gridY - (y + h);
                if (!(lx >= 0 || rx >= 0 || ty >= 0 || by >= 0)) {
                    // 相交
                    crossList.push(d);
                }
            }
        });
        return crossList;
    }
    setShadowPos(x: number, y: number, w?: number, h?: number) {
        w = w ? w : this.shadowPos.w;
        h = h ? h : this.shadowPos.h;
        console.log("----pos---");
        console.log(y);
        if (x !== this.shadowPos.gridX || y !== this.shadowPos.gridY || w !== this.shadowPos.w || h !== this.shadowPos.h) {
            console.log("进入判断");
            this.shadowPos.gridX = x;
            this.shadowPos.gridY = y;
            this.shadowPos.w = w;
            this.shadowPos.h = h;
            let sy = (this.shadowPos.gridY = this.getTopheight(this.shadowPos));
            let crossList = this.getCrossList(x, y, w, h, this.dataSet);
            // 没有相交
            if (crossList.length === 0) {
                // console.log("无相交");
                if (this.dragType === "zoom") {
                    // 改变了w, h, x
                    this.setPosition(this.shadowEle, this.shadowPos);
                } else {
                    this.setPositionByXY(this.shadowEle, this.shadowPos.gridX, this.shadowPos.gridY);
                }
            } else {
                console.log("相交");
                if (this.directionY > 0) {
                    console.log("向下");
                    // 需要向上移动的元素(找出y 小于当前元素y+h/2的列表)
                    const upList = crossList.filter(d => d.gridY <= y + h / 2);
                    console.log("ul---", upList);
                    if (upList.length > 0) {
                        upList.forEach(e => {
                            const ty = this.getTopheight(e);
                            if (ty !== e.gridY) {
                                this.setPositionByY(this.elObj[e.key], ty);
                                e.gridY = ty;
                            }
                            y = Math.max(ty + e.h, sy);
                        });
                        this.shadowPos.gridY = y;
                        sy = y;
                    }
                }
                // 需要向下移动的元素(找出y大于等于当前元素的列表,视为需要向下移动的元素)
                crossList = this.getCrossList(x, sy, w, h, this.dataSet);
                const downList = crossList.filter(d => d.gridY >= sy);
                console.log("dl---", downList);
                // 判断向下移动的元素是否已经是上下级关联关系,上下挨着的话移除下边的,用上边的级联修改
                if (downList.length > 1) {
                    // 从小到大排序
                    downList.sort((a, b) => {
                        return a.gridY - b.gridY;
                    });
                    for (let i = downList.length - 1; i > 0; i--) {
                        const eb = downList[i];
                        for (let k = i - 1; k >= 0; k--) {
                            const et = downList[k];
                            if (eb.gridY === et.gridY + et.h && (eb.gridX === et.gridX || eb.gridX + eb.w === et.gridX + et.w)) {
                                downList.pop();
                                break;
                            }
                        }
                    }
                }
                // const maxY = y;
                if (downList.length > 0) {
                    // 从半空或者从头挤下去要移动的相对高度不同
                    downList.forEach(d => {
                        const dh = sy + h - d.gridY;
                        this.changeRelative(d, dh, true);
                        d.gridY += dh;
                        this.setPositionByY(this.elObj[d.key], d.gridY);
                    });
                }
                if (this.dragType === "zoom") {
                    // this.shadowPos.gridX = x;
                    // this.shadowPos.w = w;
                    // this.shadowPos.h = h;
                    this.setPosition(this.shadowEle, this.shadowPos);
                } else {
                    // this.shadowPos.gridX = x;
                    // this.shadowPos.gridY = y;
                    this.setPositionByXY(this.shadowEle, this.shadowPos.gridX, this.shadowPos.gridY);
                }
            }
            // 依次赋值,相当于改变了DragComm里当前元素,方便计算浮动
            this.originPos.gridX = this.shadowPos.gridX;
            this.originPos.gridY = this.shadowPos.gridY;
            this.originPos.w = this.shadowPos.w;
            this.originPos.h = this.shadowPos.h;
            this.sortElePos(true);
        }
    }

    sortElePos(all?: boolean) {
        console.log("sort-pos");
        console.log(DragComm.GridPos);
        DragComm.GridPos.sort((a, b) => {
            return a.gridY - b.gridY;
        });
        DragComm.GridPos.forEach(d => {
            const h = this.getTopheight(d, all);
            // 从菜单新拉过来还没放的时候elObj对象中还没有该元素
            if (h !== d.gridY && this.elObj[d.key]) {
                this.setPositionByY(this.elObj[d.key], h);
                d.gridY = h;
            }
        });
    }
    /**
     *
     * @param e
     * @param dh
     * @param calcHalfCover 判断计算中空元素挤下去重叠的情况,只考虑第一层
     */
    changeRelative(e: ElePos, dh: number, calcHalfCover: boolean) {
        DragComm.GridPos.forEach(d => {
            const rl = d.gridX + d.w;
            const erl = e.gridX + e.w;
            if (d.key !== this.dataSet && d.gridY === e.gridY + e.h && ((rl > e.gridX && rl <= erl) || (d.gridX >= e.gridX && d.gridX < erl) || (d.gridX <= e.gridX && rl >= erl))) {
                this.changeRelative(d, dh, false);
                d.gridY += dh;
                this.setPositionByY(this.elObj[d.key], d.gridY);
            } else if (
                calcHalfCover &&
                d.key !== this.dataSet &&
                d.gridY < e.gridY + e.h + dh &&
                d.gridY > e.gridY + e.h &&
                ((rl > e.gridX && rl <= erl) || (d.gridX >= e.gridX && d.gridX < erl) || (d.gridX <= e.gridX && rl >= erl))
            ) {
                const dmh = e.gridY + e.h + dh - d.gridY;
                this.changeRelative(d, dmh, false);
                d.gridY += dmh;
                this.setPositionByY(this.elObj[d.key], d.gridY);
            }
        });
    }

    // isTopEmpty(pos: ElePos) {
    //     if (pos.gridY === 0) {
    //         return true;
    //     }
    //     const minW = pos.gridX;
    //     const maxW = pos.gridX + pos.w;
    //     const isTopUse = DragComm.GridPos.some(g => {
    //         return g.gridY + g.h <= pos.gridY && !(g.gridX >= maxW || g.gridX + g.w <= minW);
    //     });
    //     return !isTopUse;
    // }
    getTopheight(pos: ElePos, all?: boolean) {
        if (pos.gridY === 0) {
            return 0;
        }
        const minW = pos.gridX;
        const maxW = pos.gridX + pos.w;
        // 获取当前操作元素的上方元素(y小于当前元素),计算应放置的最大y值
        const topList = DragComm.GridPos.filter(g => {
            return g.key !== pos.key && (all || g.key !== this.dataSet) && g.gridY < pos.gridY && !(g.gridX >= maxW || g.gridX + g.w <= minW);
        });
        if (topList.length === 0) {
            return 0;
        } else {
            let maxH = 0;
            topList.forEach(t => {
                maxH = Math.max(t.gridY + t.h, maxH);
            });
            return maxH;
        }
    }
}

dragArea类为工作台中各模块的移动类,

import App from "@/application/app";
import { DragComm } from "./dragComm";
const boundary = 20;
const gap = 8;
export class DragArea extends DragComm {
    moveElement: HTMLElement;
    isReverse: boolean;
    permitDrag: boolean; //是否允许移动标识
    iconDom: HTMLElement;
    ssx = 0;
    ssy = 0;
    left = 0;
    top = 0;
    currentX = 0;
    currentY = 0;
    latelyClientX = 0;
    latelyClientY = 0;
    isMove = false;
    moveOrigin = 0;
    /**
     * @param source 要移动的dom
     * @param target 可以移动的范围
     * @param parent 移动元素的父元素
     */
    constructor(source: HTMLElement, parent: HTMLElement, moveElement: HTMLElement, app: App) {
        super(source, parent, app);
        this.init(moveElement);
    }
    init(moveElement: HTMLElement) {
        this.moveElement = moveElement;
        if (this.dataSet) {
            this.iconDom = this.getListIcon(this.dataSet);
        }
        this.moveElement.addEventListener("mousedown", this.mousedown);
    }
    mousedown = (e: MouseEvent) => {
        console.log("--area--down");
        if (this.source.parentNode !== this.parent) {
            return;
        }
        e.stopPropagation();
        this.permitDrag = true;
        this.dragType = "drag";
        this.isReverse = this.app.position === "left";
        document.addEventListener("mousemove", this.mousemove);
        document.addEventListener("mouseup", this.mouseup);

        this.initMouseDown();
        console.log(DragComm.GridPos);

        this.ssx = this.x;
        this.ssy = this.y;
        this.left = e.clientX - this.x;
        this.top = e.clientY - this.y;

        this.latelyClientX = e.clientX;
        this.latelyClientY = e.clientY;
    };
    mousemove = (e: MouseEvent) => {
        if (!this.permitDrag) return false;
        const now = new Date().getTime();
        // if (now - this.time < 50) {
        //     return false;
        // }
        this.time = now;
        this.isMove = true;

        // console.log("--area--move");

        this.shadowEle.style.display = "";

        this.directionX = e.clientX - this.latelyClientX;
        this.directionY = e.clientY - this.latelyClientY;
        this.latelyClientX = e.clientX;
        this.latelyClientY = e.clientY;

        let x = e.clientX - this.left;
        let y = e.clientY - this.top;

        const maxL = this.pw - this.w;
        // const maxT = this.ph - this.h;

        x = x > maxL ? maxL : x;
        x = x < 0 ? 0 : x;
        // y = y > maxT ? maxT : y;
        y = y < 0 ? 0 : y;

        this.source.style.left = x + "px";
        this.source.style.top = y + "px";

        const gx = Math.round(x / this.prx);
        const gy = Math.round(y / this.pry);

        this.setShadowPos(gx, gy, this.originPos.w, this.originPos.h);

        if (this.dataSet != "0") {
            // 移出
            if ((e.clientX < this.px && !this.isReverse) || (e.clientX > this.px + this.pw && this.isReverse)) {
                //
                this.mouseup(e);
                this.remove(this.source);
                DragComm.GridPos = DragComm.GridPos.filter(d => d.key != this.dataSet);
                this.sortElePos();
                this.iconDom.style.left = e.clientX - 20 + "px";
                this.iconDom.style.top = e.clientY - 20 + "px";
                this.iconDom.style.pointerEvents = "none";
                this.iconDom.style.display = "";
                this.iconDom.dispatchEvent(new MouseEvent("mousedown", { clientX: e.clientX, clientY: e.clientY }));
            }
        }
    };
    getListIcon(key: string) {
        const iconDomLi = [...document.querySelectorAll(".vq-wrapper-content-left-list-li")] as HTMLElement[];
        const list = iconDomLi.filter(li => {
            return li.dataset.i === key;
        });
        if (list.length > 0) {
            return list[0];
        }
    }
    mouseup = (e: MouseEvent) => {
        console.log("--area--up");
        e.stopPropagation();

        this.permitDrag = false;
        this.isMove = false;
        this.initMouseUp();

        document.removeEventListener("mousemove", this.mousemove);
        document.removeEventListener("mouseup", this.mouseup);
    };

    
}

 DragMenu为菜单拖动类,该类是移动面板左侧的菜单icon进行菜单icon排序,还有就是拖动进入模块区域后生成模块shadow对模块进行展开

import App from "@/application/app";
import { DragArea } from "./drag.area";
import { DragComm } from "./dragComm";
export class DragMenu extends DragComm {
    // source: HTMLElement; // 源容器
    target: HTMLElement; //允许放置的容器
    isReverse: boolean;
    moveElement: HTMLElement; //允许拖拽的位置
    permitDrag: boolean; //是否允许移动标识
    childrenList: HTMLElement[];
    childrenPosition: number[];
    x = 0;
    y = 0;
    left = 0;
    top = 0;
    currentX = 0;
    currentY = 0;
    isMove = false;
    time: number = null;
    seat: HTMLElement;
    seatIndex = -1;
    /**
     * @param source 要移动的dom
     * @param target 可以移动的范围
     * @param menuListEle 移动元素的父元素
     * @param boundaryEle 移动元素的目的地
     * @param putIn 卡片移入右侧主区域
     */
    timeOut: NodeJS.Timeout = null;
    constructor(source: HTMLElement, target: string | HTMLElement, public menuListEle: HTMLElement, public boundaryEle: HTMLElement, public putIn: () => any, public putOut: () => any, app: App) {
        super(source, boundaryEle, app);
        this.init(source, target);
    }
    init(source: HTMLElement, target: string | HTMLElement, moveElement?: HTMLElement | string) {
        this.source = typeof source === "string" ? document.querySelector(source) : source;
        this.target = typeof target === "string" ? document.querySelector(target) : target;
        if (moveElement) {
            this.moveElement = typeof moveElement === "string" ? document.querySelector(moveElement) : moveElement;
            this.moveElement.addEventListener("mousedown", this.mousedown);
        } else {
            this.source.addEventListener("mousedown", this.mousedown);
        }
    }
    mousedown = (e: MouseEvent) => {
        e.stopPropagation();
        document.addEventListener("mousemove", this.mousemove);
        document.addEventListener("mouseup", this.mouseup);
        this.isReverse = this.app.position === "left";
        this.seatIndex = -1;
        if (e.isTrusted) {
            this.timeOut = setTimeout(() => {
                this.setMouseDown(e);
            }, 200);
        } else {
            this.setMouseDown(e);
        }
    };
    setMouseDown(e: MouseEvent) {
        console.log("--menu--down");
        this.clearUpTime();
        this.permitDrag = true;

        // 分割的每小块的尺寸  计算放置位置用
        const { x: px, y: py, w: pw, h: ph } = this.getScale(this.boundaryEle);
        this.px = px;
        this.py = py;
        this.prx = pw / this.tx;
        this.pry = ph / this.ty;

        const { x, y } = this.getScale(this.source);
        this.x = x;
        this.y = y;
        this.currentX = x;
        this.currentY = y;
        this.left = e.clientX - this.x;
        this.top = e.clientY - this.y;
        this.source.style.position = "fixed";
        this.source.style.left = this.x + "px";
        this.source.style.top = this.y + "px";

        this.source.style.padding = "10px";
        this.source.style.borderRadius = "50%";
        this.source.style.background = "#fff";
        this.source.style.boxShadow = "0 0 5px rgba(0, 0, 0, 0.12)";

        this.source.style.transform = "scale(1.3)";
        this.source.style.transition = "none";
        this.source.style.pointerEvents = "none";
        this.source.style.zIndex = "100";
        document.body.style.userSelect = "none";

        this.time = new Date().getTime();
        this.marker = document.createElement("div");
        this.marker.classList.add("vq-drag-maker");
        this.marker.style.position = "fixed";
        this.marker.style.top = "0";
        this.marker.style.left = "0";
        this.marker.style.bottom = "0";
        this.marker.style.width = "100%";
        this.marker.style.height = "100%";
        this.marker.style.right = "0";
        this.marker.style.zIndex = "10000000";

        const list = [...this.menuListEle.children] as HTMLElement[];
        this.childrenList = list.filter(li => {
            return li.style.position != "fixed";
        });
        this.childrenPosition = [];
        this.childrenList.forEach(li => {
            const { y, h } = this.getScale(li);
            this.childrenPosition.push(y + h);
        });

        this.seat = document.createElement("div");
        this.seat.id = "vq-seat";
        this.seat.style.height = "2px";
        this.seat.style.margin = "0 8px";
        this.seat.style.background = "#000";

        this.shadowPos = {
            key: this.dataSet,
            w: this.initw,
            h: this.inith,
            gridX: -1, // 防止初始值不变不能触发setShadowPos计算
            gridY: 0,
        };
        // mousedown时先建立坐标
        this.originPos = { ...this.shadowPos };
        DragComm.GridPos.push(this.originPos);
        this.shadowEle = this.boundaryEle.querySelector(".vq-shadow-follow");
        if (!this.shadowEle) {
            this.shadowEle = this.create("div", "vq-shadow-follow", this.boundaryEle);
            this.shadowEle.innerHTML = "<span>松手即可置入面板</span>";
            this.create("div", "vq-put-in", this.shadowEle);
            this.shadowEle.style.display = "none";
            this.setPositionByWH(this.shadowEle, this.initw, this.inith);
        }
        this.setEleList();

        document.body.appendChild(this.marker);
    }
    mousemove = (e: MouseEvent) => {
        if (!this.permitDrag) return false;
        this.isMove = true;
        console.log("--menu--move");
        let x = e.clientX - this.left;
        let y = e.clientY - this.top;
        const { w, h } = this.getScale(this.source);
        const { x: tx, y: ty, w: tw, h: th } = this.getScale(this.target);
        const { x: px, y: py, w: pw, h: ph } = this.getScale(this.menuListEle);

        //边界判断
        x = x < tx ? tx : x;
        x = x > tx + tw - w ? tx + tw - w : x;
        y = y < ty ? ty : y;
        y = y > ty + th - h ? ty + th - h : y;
        this.currentX = x;
        this.currentY = y;
        this.source.style.left = x + "px";
        this.source.style.top = y + "px";

        // 排序
        if ((!this.isReverse && x < px + pw) || (this.isReverse && x + w > px)) {
            // 鼠标移出时,计算位置时忽略当前待移入dom
            this.sortElePos(false);
            this.shadowEle.style.display = "none";
            for (let i = 0; i < this.childrenPosition.length; i++) {
                if (y + h / 2 < this.childrenPosition[i] || i === this.childrenPosition.length - 1) {
                    this.remove(this.seat);
                    this.insert(this.menuListEle, this.seat, this.childrenList[i]);
                    this.seatIndex = i;
                    return;
                }
            }
        } else {
            this.shadowEle.style.display = "";
            // move移出时gridX变成0,再移入时到相同位置时坐标不变不触发setShadowPos计算
            this.shadowPos.gridX = -1;
            this.remove(this.seat);
            this.showPlace(e.clientX, e.clientY);
        }
    };
    mouseup = (e: MouseEvent) => {
        console.log("--menu--up");
        e.stopPropagation();
        document.removeEventListener("mousemove", this.mousemove);
        document.removeEventListener("mouseup", this.mouseup);
        if (!this.permitDrag) {
            clearTimeout(this.timeOut);
        } else {
            const { x: px, y: py, w: pw, h: ph } = this.getScale(this.menuListEle);
            const { w } = this.getScale(this.source);
            if ((!this.isReverse && this.currentX < px + pw) || (this.isReverse && this.currentX + w > px)) {
                // 移出
                this.source.style = null;
                if (this.isMove && this.seatIndex >= 0) {
                    this.remove(this.source);
                    this.insert(this.menuListEle, this.source, this.childrenList[this.seatIndex]);
                    this.putOut();
                }
                // 移出的话删除坐标
                DragComm.GridPos = DragComm.GridPos.filter(d => d.key !== this.dataSet);
                if (this.shadowEle) {
                    this.remove(this.shadowEle);
                }
            } else {
                // 移入的话,坐标不做处理,mousedown已经添加坐标
                const { x: sx, y: sy, w: sw, h: sh } = this.getScale(this.shadowEle);
                this.source.style.boxShadow = "";
                this.source.style.transition = "left .1s ease, top .1s ease, opacity .5s linear";
                this.source.style.left = sx + sw / 2 - 20 + "px";
                this.source.style.top = sy + sh / 2 - 20 + "px";

                const toCenter = () => {
                    // this.source.style.transition = "opacity .5s linear";
                    this.source.removeEventListener("transitionend", toCenter);
                    this.source.style.opacity = "0";

                    // 图标消失渐变
                    const callback = () => {
                        console.log("渐变");
                        this.source.style.opacity = "1";
                        this.source.style.display = "none";
                        this.source.removeEventListener("transitionend", callback);
                    };
                    this.source.addEventListener("transitionend", callback);

                    console.log(DragComm.GridPos);
                    if (this.shadowEle) {
                        // const { x, sx, y: sy, w: sw, h: sh } = this.geOffset(this.shadowEle);
                        // 设置圆覆盖动画
                        const ppw = Math.ceil(Math.pow(Math.pow(sw / 2, 2) + Math.pow(sh / 2, 2), 0.5) * 2);
                        const pt = this.shadowEle.querySelector(".vq-put-in");
                        pt.style.transition = "all .2s linear";
                        pt.style.top = `calc(50% - ${ppw / 2}px)`;
                        pt.style.left = `calc(50% - ${ppw / 2}px)`;
                        pt.style.width = ppw + "px";
                        pt.style.height = ppw + "px";
                        const ptEnd = () => {
                            console.log("----shadd---trs--end");
                            this.putIn();
                            this.initPutInEle(this.dataSet);
                            this.remove(this.shadowEle);
                            pt.removeEventListener("transitionend", ptEnd);
                        };
                        pt.addEventListener("transitionend", ptEnd);
                    } else {
                        this.putIn();
                        this.initPutInEle(this.dataSet);
                        this.remove(this.shadowEle);
                    }
                };
                this.source.addEventListener("transitionend", toCenter);
            }
            if (this.marker) {
                document.body.removeChild(this.marker);
                this.marker = null;
            }
            const makerLi = [...document.querySelectorAll(".vq-drag-maker")] as HTMLElement[];
            makerLi.forEach(m => {
                this.remove(m);
            });
            if (this.seat) {
                this.remove(this.seat);
            }
            // if (this.shadowEle) {
            //     this.remove(this.shadowEle);
            // }
            this.permitDrag = false;
            this.isMove = false;
            document.body.style.userSelect = "";
            this.saveLayout();
        }
    };
    saveLayout() {
        const list = this.getChildren(this.menuListEle);
        App.layoutPos.lisPos = [];
        list.forEach(d => {
            const key = d.dataset.i;
            App.layoutPos.lisPos.push(key);
        });
        App.layoutPos.elePos = DragComm.GridPos;
        App.setMouseUpTimer();
    }
}

 zoomArea:通过模块左下角调整模块的宽高,当宽高变化时同样需要进行setShadowPos判断

import App from "@/application/app";
import { LayoutPos } from "@/application/app.type";
import { DragArea } from "./drag.area";
import { DragComm } from "./dragComm";
const boundary = 30;
const gap = 15;
const body = document.body || document.documentElement;
export class ZoomArea extends DragComm {
    moveElement: HTMLElement;
    isReverse: boolean;
    permitDrag: boolean; //是否允许移动标识
    ssw = 0;
    ssh = 0;
    left = 0;
    top = 0;
    br: HTMLElement;
    bf: HTMLElement;
    type: string = null;
    constructor(source: HTMLElement, parent: HTMLElement, moveElement: HTMLElement, app: App) {
        super(source, parent, app);
        this.init(moveElement, app);
    }
    init(moveElement: HTMLElement, app: App) {
        this.moveElement = moveElement;
        this.clearDom();
        new DragArea(this.source, this.parent, this.moveElement, app);
        // const br = (this.br = this.create("i", "vq-zoom vq-zoom-br", c));
        const bf = (this.bf = this.create("i", "vq-zoom vq-zoom-bf", this.source));
        // br.addEventListener("mousedown", (e: MouseEvent) => {
        //     this.type = "br";
        //     this.mousedown(e);
        // });
        bf.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "bf";
            this.mousedown(e);
        });
    }
    mousedown = (e: MouseEvent) => {
        console.log("--zoomarea--down--");
        if (this.source.parentNode !== this.parent) {
            return;
        }
        e.stopPropagation();
        this.permitDrag = true;
        this.dragType = "zoom";
        this.isReverse = this.app.position === "left";
        document.addEventListener("mousemove", this.mousemove);
        document.addEventListener("mouseup", this.mouseup);

        this.initMouseDown();

        this.left = e.clientX;
        this.top = e.clientY;
    };
    mousemove = (e: MouseEvent) => {
        console.log("--zoomarea--move--");
        if (!this.permitDrag) return false;
        // const now = new Date().getTime();
        // if (now - this.time < 100) {
        //     return false;
        // }
        // this.time = now;

        this.shadowEle.style.display = "";

        const mouseMoveX = e.clientX;
        const mouseMoveY = e.clientY;

        let w = 0;
        let h = 0;

        if (this.type === "bf") {
            w = this.w + this.left - mouseMoveX;
            w = w < 0 ? 0 : w;
            h = this.h - (this.top - mouseMoveY);
            h = h < 0 ? 0 : h;

            let sw = Math.round((w + gap) / this.prx);
            let sh = Math.round((h + gap) / this.pry);
            sw = sw < this.mw ? this.mw : sw;
            sw = sw > this.originPos.gridX + this.originPos.w ? this.originPos.gridX + this.originPos.w : sw;
            sh = sh < this.mh ? this.mh : sh;
            const sx = this.shadowPos.gridX + this.shadowPos.w - sw;
            const sy = this.shadowPos.gridY;
            this.setShadowPos(sx, sy, sw, sh);

            this.source.style.width = w + "px";
            this.source.style.height = h + "px";
            this.source.style.left = this.x + this.w - w + "px";
        }
    };
    mouseup = (e: MouseEvent) => {
        console.log("--zoomarea--up--");
        this.permitDrag = false;
        this.initMouseUp();

        document.removeEventListener("mousemove", this.mousemove);
        document.removeEventListener("mouseup", this.mouseup);
    };
    clearDom() {
        const s = [...this.source.children];
        s.forEach(z => {
            if (z.classList.contains("vq-zoom")) {
                this.remove(z);
            }
        });
    }
}

 domUtil:dom操作类

import * as Util from "./util";

export class DomUtil {
    checkDomLoad(sel: string, callback: () => any) {
        let timer: NodeJS.Timeout = null;
        function check() {
            const dom = document.querySelector(sel);
            if (dom) {
                //  执行dom加载完成后的操作,例如echart的初始化操作
                //  清除定时器
                callback();
                if (!timer) {
                    clearTimeout(timer);
                }
            } else {
                //  自我调用
                timer = setTimeout(check, 0);
            }
        }
        check();
    }
    // 获取dom
    get(selector: string): HTMLElement {
        return document.querySelector(selector);
    }
    createElement(tagName: string, className: string, html = ""): HTMLElement {
        const el = document.createElement(tagName);
        el.className = className || "";
        el.innerHTML = html;
        return el;
    }
    nextTick(fn: () => any) {
        Promise.resolve().then(() => {
            fn();
        });
    }
    /**
     *
     * @param tagName
     * @param className  创建dom
     * @param container
     */
    create(tagName: string, className: string, container?: HTMLElement): HTMLElement {
        const el = document.createElement(tagName);
        el.className = className || "";
        if (container) {
            container.appendChild(el);
        }
        return el;
    }
    createSvg(src: string, className: string, container?: HTMLElement): HTMLEmbedElement {
        // HTMLOrSVGElement;
        const el: HTMLEmbedElement = document.createElement("embed");
        el.className = className || "";
        el.src = src;
        if (container) {
            container.appendChild(el);
        }
        return el;
    }
    /**
     *
     * @param {*} el  删除元素
     */
    remove(el: HTMLElement | Node) {
        if (!el) return;
        const parent = el.parentNode;
        if (parent) {
            parent.removeChild(el);
        }
    }
    /**
     *
     * @param {*} el  清空子元素
     */
    empty(el: HTMLElement) {
        while (el.firstChild) {
            el.removeChild(el.firstChild);
        }
    }
    /**
     *
     * @param {*} el 变成最后一个儿子
     */
    toFront(el: HTMLElement) {
        const parent = el.parentNode;
        if (parent && parent.lastChild !== el) {
            parent.appendChild(el);
        }
    }
    // 变成第一个儿子
    toBack(el: HTMLElement) {
        const parent = el.parentNode;
        if (parent && parent.firstChild !== el) {
            parent.insertBefore(el, parent.firstChild);
        }
    }
    slideIn(el: HTMLElement, isReverse: boolean) {
        const className1 = isReverse ? "vq-slideIn-reverse" : "vq-slideIn";
        const className2 = isReverse ? "vq-slideOut-reverse" : "vq-slideOut";
        this.addClass(el, className1);
        this.removeClass(el, className2);
        el.style.display = "";
    }
    slideOut(el: HTMLElement, isReverse: boolean) {
        const className1 = isReverse ? "vq-slideIn-reverse" : "vq-slideIn";
        const className2 = isReverse ? "vq-slideOut-reverse" : "vq-slideOut";
        if (el.style.display === "none") {
            return;
        }
        this.removeClass(el, className1);
        this.addClass(el, className2);
        const remove = () => {
            el.style.display = "none";
            el.removeEventListener("webkitAnimationEnd", remove);
            el.removeEventListener("animationend", remove);
            el.removeEventListener("oAnimationEnd", remove);
            this.removeClass(el, className2);
        };
        el.addEventListener("webkitAnimationEnd", remove);
        el.addEventListener("animationend", remove);
        el.addEventListener("oAnimationEnd", remove);

        // Promise.resolve().then(() => { });
    }
    show(el: HTMLElement, className1 = "vq-fadeIn", className2 = "vq-fadeout") {
        if (!el) {
            return;
        }
        this.addClass(el, className1);
        this.removeClass(el, className2);
        el.style.display = "";
    }
    hidden(el: HTMLElement, className1 = "vq-fadeIn", className2 = "vq-fadeout") {
        if (!el) {
            return;
        }
        if (el.style.display === "none") {
            return;
        }
        this.removeClass(el, className1);
        this.addClass(el, className2);
        const remove = () => {
            el.style.display = "none";
            el.removeEventListener("webkitAnimationEnd", remove);
            el.removeEventListener("animationend", remove);
            el.removeEventListener("oAnimationEnd", remove);
        };
        el.addEventListener("webkitAnimationEnd", remove);
        el.addEventListener("animationend", remove);
        el.addEventListener("oAnimationEnd", remove);

        // Promise.resolve().then(() => { });
    }

    cardShow(el: HTMLElement, className = "vq-card-show") {
        if (!el) {
            return;
        }
        if (el.style.display === "none") {
            return;
        }
        this.addClass(el, className);
        const remove = () => {
            this.removeClass(el, className);
            el.removeEventListener("webkitAnimationEnd", remove);
            el.removeEventListener("animationend", remove);
            el.removeEventListener("oAnimationEnd", remove);
        };
        el.addEventListener("webkitAnimationEnd", remove);
        el.addEventListener("animationend", remove);
        el.addEventListener("oAnimationEnd", remove);
    }
    /**
     * 父节点下 特定class的第index个子元素背景闪动
     * @param target 要设置动画的元素
     * @param parent 父元素 通过父元素和下标获取目标元素
     * @param index 设置动画的子元素下标
     * @param specificClass 父元素下特定class的子元素
     */
    backgroundFlash({ target, parent, index, specificClass }: { target?: HTMLElement; parent?: HTMLElement; index?: number; specificClass?: string }) {
        let ele: HTMLElement = null;
        if (target) {
            ele = target;
        } else if (parent) {
            let list = [];
            if (specificClass) {
                list = [...parent.querySelectorAll(`.${specificClass}`)] as HTMLElement[];
            } else {
                list = [...parent.childNodes] as HTMLElement[];
            }

            ele = list[index];
        }
        this.addClass(ele, "vq-background-flash");
        const remove = () => {
            this.removeClass(ele, "vq-background-flash");
            ele.removeEventListener("webkitAnimationEnd", remove);
            ele.removeEventListener("animationend", remove);
            ele.removeEventListener("oAnimationEnd", remove);
        };
        ele.addEventListener("webkitAnimationEnd", remove);
        ele.addEventListener("animationend", remove);
        ele.addEventListener("oAnimationEnd", remove);
    }
    /**
     *
     * @param {*} el
     * @param {*} name  判断className是否存在
     */
    hasClass(el: HTMLElement, name: string) {
        if (el.classList !== undefined) {
            return el.classList.contains(name);
        }
        const className = this.getClass(el);
        return className.length > 0 && new RegExp("(^|\\s)" + name + "(\\s|$)").test(className);
    }
    /**
     *
     * @param {*} el
     * @param {*} name 添加className
     */
    addClass(el: HTMLElement, name: string) {
        if (el.classList !== undefined) {
            const classes = Util.splitWords(name);
            for (let i = 0, len = classes.length; i < len; i++) {
                el.classList.add(classes[i]);
            }
        } else if (!this.hasClass(el, name)) {
            const className = this.getClass(el);
            this.setClass(el, (className ? className + " " : "") + name);
        }
    }
    insert(parent: Node, c1: HTMLElement, c2?: HTMLElement) {
        if (c2) {
            parent.insertBefore(c1, c2);
        } else {
            parent.appendChild(c1);
        }
    }
    /**
     *
     * @param {*} el  删除className
     * @param {*} name
     */
    removeClass(el: HTMLElement, name: string) {
        if (!el) {
            return;
        }
        if (el.classList !== undefined) {
            el.classList.remove(name);
        } else {
            this.setClass(el, Util.trim((" " + this.getClass(el) + " ").replace(" " + name + " ", " ")));
        }
    }
    /**
     *
     * @param {*} el
     * @param {*} name  设置className
     */
    setClass(el: HTMLElement, name: string) {
        el.className = name;
    }
    /**
     *
     * @param {*} el 获取className
     */
    getClass(el: HTMLElement) {
        return el.className || "";
    }
    /**
     *
     * @param {*} element  查找父亲大小不为空的元素
     */
    getSizedParentNode(element: HTMLElement) {
        do {
            element = element.parentNode as HTMLElement;
        } while ((!element.offsetWidth || !element.offsetHeight) && element !== document.body);
        return element;
    }
    // 获取元素位置
    getScale(element: HTMLElement) {
        if (!element) {
            return {
                x: 0,
                y: 0,
                w: 0,
                h: 0,
            };
        }
        const rect = element.getBoundingClientRect(); // Read-only in old browsers.
        return {
            x: rect.x,
            y: rect.y,
            w: rect.width,
            h: rect.height,
            bottom: rect.bottom,
        };
    }
    geOffset(element: HTMLElement) {
        return {
            x: element.offsetLeft,
            y: element.offsetTop,
            w: element.offsetWidth,
            h: element.offsetHeight,
        };
    }
    findChild(parentNode: HTMLElement, childNode: any): HTMLElement {
        if (childNode.parentNode === parentNode) {
            return childNode;
        }
        while ((childNode = childNode.parentNode)) {
            if (parentNode === childNode.parentNode) {
                return childNode;
            }
        }
        return null;
    }
    getEle(selector: string): HTMLElement {
        const ele = document.querySelector<HTMLElement>(selector);
        if (ele) {
            return ele;
        }
    }
    getChildren(parentNode: HTMLElement): HTMLElement[] {
        const children: HTMLElement[] = [...parentNode.children] as HTMLElement[];
        return children;
    }
}
// const animations = {
//     animation: "animationend",
//     OAnimation: "oAnimationEnd",
//     MozAnimation: "animationend",
//     WebkitAnimation: "webkitAnimationEnd",
// };

// for (t in animations) {
//     if (el.style[t] !== undefined) {
//         return animations[t];
//     }
// }
import App from "@/application/app";
import { DomUtil } from "@/utils/domUtil";
const boundary = 10;

export class DragBase extends DomUtil {
    public static mouseUpTimer: NodeJS.Timeout;
    source: HTMLElement; // 源容器
    target: HTMLElement; //允许放置的容器
    moveElement: HTMLElement; //允许拖拽的位置
    permitDrag: boolean; //是否允许移动标识
    x = 0;
    y = 0;
    left = 0;
    top = 0;
    currentX = 0;
    currentY = 0;
    isMove = false;
    time: number = null;
    marker: HTMLElement;
    constructor(source: string | HTMLElement, target: string | HTMLElement, moveElement?: HTMLElement | string, public app?: App) {
        super();
        this.init(source, target, moveElement);
    }
    init(source: string | HTMLElement, target: string | HTMLElement, moveElement: HTMLElement | string) {
        this.source = typeof source === "string" ? document.querySelector(source) : source;
        this.target = typeof target === "string" ? document.querySelector(target) : target;
        if (moveElement) {
            this.moveElement = typeof moveElement === "string" ? document.querySelector(moveElement) : moveElement;
            this.moveElement.addEventListener("mousedown", this.mousedown);
        } else {
            this.source.addEventListener("mousedown", this.mousedown);
        }
    }
    mouseup = (e: MouseEvent) => {
        e.stopPropagation();

        this.permitDrag = false;
        this.isMove = false;
        this.source.style.background = "";
        this.source.style.transition = "";
        document.body.style.userSelect = "";
        if (this.marker) {
            document.body.removeChild(this.marker);
        }
        document.removeEventListener("mousemove", this.mousemove);
        document.removeEventListener("mouseup", this.mouseup);
        if (this.app) {
            this.saveLayout();
        }
    };

    mousemove = (e: MouseEvent) => {
        if (!this.permitDrag) return false;

        this.source.style.transition = "none";

        let x = e.pageX - this.left;
        let y = e.pageY - this.top;
        const { w, h } = this.getScale(this.source);
        const maxL = window.innerWidth - w - boundary;
        const maxT = window.innerHeight - h - boundary;
        //边界判断
        x = x < boundary ? 0 : x;
        x = x > maxL ? window.innerWidth - w : x;
        y = y > maxT ? window.innerHeight - h : y;
        y = y < boundary ? 0 : y;
        this.currentX = x;
        this.currentY = y;
        this.source.style.left = x + "px";
        this.source.style.top = y + "px";
        this.isMove = true;
    };
    mousedown = (e: MouseEvent) => {
        e.stopPropagation();
        if (this.app) {
            this.clearUpTime();
        }
        document.addEventListener("mousemove", this.mousemove);
        document.addEventListener("mouseup", this.mouseup);
        const selectEle = document.querySelector(".vq-select-area") as HTMLElement;
        if (selectEle) {
            selectEle.style.display = "none";
        }
        const { x, y } = this.getScale(this.source);
        this.permitDrag = true;
        this.x = x;
        this.y = y;
        this.left = e.pageX - this.x;
        this.top = e.pageY - this.y;
        this.source.style.position = "fixed";
        this.source.style.left = this.x + "px";
        this.source.style.top = this.y + "px";

        this.time = new Date().getTime();
        this.source.style.transform = "none";
        this.source.style.transition = "none";
        document.body.style.userSelect = "none";
        this.marker = document.createElement("div");
        this.marker.style.position = "fixed";
        this.marker.style.top = "0";
        this.marker.style.left = "0";
        this.marker.style.bottom = "0";
        this.marker.style.width = "100%";
        this.marker.style.height = "100%";
        this.marker.style.right = "0";
        this.marker.style.zIndex = "1000";

        document.body.appendChild(this.marker);
    };
    clearUpTime() {
        clearTimeout(App.mouseUpTimer);
    }
    saveLayout() {
        const { x, y, w, h } = this.getScale(this.source);
        App.layoutPos.wraPos.left = x;
        App.layoutPos.wraPos.top = y;
        console.log("----drag--base--save");
        console.log(App.layoutPos);
        App.setMouseUpTimer();
    }
}
import App from "@/application/app";
import { DomUtil } from "@/utils/domUtil";
const body = document.body || document.documentElement;
const min = 200;
export class Zoom extends DomUtil {
    source: HTMLElement; // 源容器
    boundaryEle?: HTMLElement; // 相互依赖元素
    isReverse?: boolean;
    isSave: boolean; // 元素位置是否需要保存,即是否为主面板
    permitDrag: boolean; //是否允许移动标识
    bw = 0;
    bx = 0;
    x = 0;
    y = 0;
    left = 0;
    top = 0;
    w = 0;
    h = 0;
    right = 0;
    bottom = 0;
    pw = 0;
    ph = 0;
    tf: HTMLElement;
    tr: HTMLElement;
    bf: HTMLElement;
    br: HTMLElement;
    le: HTMLElement;
    re: HTMLElement;
    te: HTMLElement;
    be: HTMLElement;
    marker: HTMLElement;
    isMove = false;
    type: string = null;
    constructor(source: string | HTMLElement, isSave: boolean, boundaryEle?: string | HTMLElement, public app?: App) {
        super();
        this.init(source, isSave, boundaryEle);
    }
    init(source: string | HTMLElement, isSave: boolean, boundaryEle?: string | HTMLElement) {
        this.source = typeof source === "string" ? document.querySelector(source) : source;
        if (boundaryEle) {
            this.boundaryEle = typeof boundaryEle === "string" ? document.querySelector(boundaryEle) : boundaryEle;
        }
        this.isSave = isSave;
        this.clearDom();
        const tf = (this.tf = this.create("i", "vq-zoom vq-zoom-tf", this.source));
        const tr = (this.tr = this.create("i", "vq-zoom vq-zoom-tr", this.source));
        const bf = (this.bf = this.create("i", "vq-zoom vq-zoom-bf", this.source));
        const br = (this.br = this.create("i", "vq-zoom vq-zoom-br", this.source));
        const le = (this.le = this.create("i", "vq-zoom vq-zoom-le", this.source));
        const re = (this.re = this.create("i", "vq-zoom vq-zoom-re", this.source));
        const te = (this.te = this.create("i", "vq-zoom vq-zoom-te", this.source));
        const be = (this.be = this.create("i", "vq-zoom vq-zoom-be", this.source));
        tf.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "tf";
            this.mousedown(e);
        });
        tr.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "tr";
            this.mousedown(e);
        });
        bf.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "bf";
            this.mousedown(e);
        });
        br.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "br";
            this.mousedown(e);
        });
        le.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "le";
            this.mousedown(e);
        });
        re.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "re";
            this.mousedown(e);
        });
        te.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "te";
            this.mousedown(e);
        });
        be.addEventListener("mousedown", (e: MouseEvent) => {
            this.type = "be";
            this.mousedown(e);
        });
    }
    mouseup = (e: MouseEvent) => {
        this.permitDrag = false;
        this.source.style.userSelect = "";
        // this.source.style.transition = "all .3s linear";
        document.removeEventListener("mousemove", this.mousemove);
        document.removeEventListener("mouseup", this.mouseup);
        body.style.userSelect = "";
        body.removeChild(this.marker);
        if (this.isSave && this.app) {
            this.saveLayout();
        }
    };

    mousemove = (e: MouseEvent) => {
        if (!this.permitDrag) return false;
        // const mouseMoveX = e.clientX;
        // const mouseMoveY = e.clientY;
        // const offsetLeft = this.source.offsetLeft;
        // const offsetTop = this.source.offsetTop;

        const dltX = e.clientX - this.x;
        const dltY = e.clientY - this.y;
        let width = 0;
        let height = 0;

        if (this.type === "br") {
            // 右下角
            width = dltX + this.w;
            height = dltY + this.h;
            if (height < min) {
                height = min;
                return;
            }
            if (width < min) {
                width = min;
                return;
            }
            if (dltX > this.right) {
                return;
            }
            if (dltY > this.bottom) {
                return;
            }
        }
        if (this.type === "bf") {
            // 左下角
            width = this.w - dltX;
            height = this.h + dltY;
            const left = this.left + dltX;
            if (height < min) {
                height = min;
                return;
            }
            if (width < min) {
                width = min;
                return;
            }
            if (-dltX > this.left) {
                return;
            }
            if (dltY > this.bottom) {
                return;
            }
            this.source.style.left = left + "px";
        }
        if (this.type === "tr") {
            // 右上角
            width = this.w + dltX;
            height = this.h - dltY;
            const top = this.top + dltY;
            if (height < min) {
                height = min;
                return;
            }
            if (width < min) {
                width = min;
                return;
            }
            if (dltX > this.right) {
                return;
            }
            if (-dltY > this.top) {
                return;
            }
            this.source.style.top = top + "px";
        }
        if (this.type === "tf") {
            // 左上角
            width = this.w - dltX;
            height = this.h - dltY;
            const left = this.left + dltX;
            const top = this.top + dltY;
            if (height < min) {
                height = min;
                return;
            }
            if (width < min) {
                width = min;
                return;
            }
            if (-dltX > this.left) {
                return;
            }
            if (-dltY > this.top) {
                return;
            }
            this.source.style.left = left + "px";
            this.source.style.top = top + "px";
        }
        if (this.type === "le") {
            // 左边
            width = this.w - dltX;
            height = this.h;
            const left = this.left + dltX;

            if (width < min) {
                width = min;
                return;
            }
            if (-dltX > this.left) {
                return;
            }
            this.source.style.left = left + "px";
        }
        if (this.type === "re") {
            // 右边
            width = this.w + dltX;
            height = this.h;
            if (width < min) {
                width = min;
                return;
            }
            if (dltX > this.right) {
                return;
            }
        }
        if (this.type === "be") {
            // 下边
            width = this.w;
            height = this.h + dltY;

            if (height < min) {
                height = min;
                return;
            }
            if (dltY > this.bottom) {
                return;
            }
        }
        if (this.type === "te") {
            // 上边
            width = this.w;
            height = this.h - dltY;
            const top = this.top + dltY;

            if (height < min) {
                height = min;
                return;
            }
            if (-dltY > this.top) {
                return;
            }
            this.source.style.top = top + "px";
        }

        this.source.style.width = width + "px";
        this.source.style.height = height + "px";
    };
    mousedown = (e: MouseEvent) => {
        e.stopPropagation();
        if (this.isSave && this.app) {
            this.clearUpTime();
        }
        const selectEle = document.querySelector(".vq-select-area") as HTMLElement;
        if (selectEle) {
            selectEle.style.display = "none";
        }
        this.permitDrag = true;
        if (this.app) {
            this.isReverse = this.app.position === "left";
        }
        this.source.style.userSelect = "none";
        body.style.userSelect = "none";
        document.addEventListener("mousemove", this.mousemove);
        document.addEventListener("mouseup", this.mouseup);
        const { x, y, w, h } = this.getScale(this.source);
        if (this.boundaryEle) {
            const { x: bx, w: bw } = this.getScale(this.boundaryEle);
            this.bx = bx;
            this.bw = bw;
        }
        this.left = x;
        this.top = y;
        this.w = w;
        this.h = h;
        this.x = e.clientX;
        this.y = e.clientY;
        const pw = window.innerWidth;
        const ph = window.innerHeight;
        this.right = pw - x - w;
        this.bottom = ph - y - h;
        this.marker = document.createElement("div");
        this.marker.style.position = "fixed";
        this.marker.style.top = "0";
        this.marker.style.left = "0";
        this.marker.style.bottom = "0";
        this.marker.style.width = "100%";
        this.marker.style.height = "100%";
        this.marker.style.right = "0";
        this.marker.style.zIndex = "10";

        body.appendChild(this.marker);
    };
    clearDom() {
        const zoomLl = [...this.source.children];
        zoomLl.forEach(z => {
            if (z.classList.contains("vq-zoom")) {
                this.remove(z);
            }
        });
    }
    clearUpTime() {
        clearTimeout(App.mouseUpTimer);
    }
    saveLayout() {
        const { x, y, w, h } = this.getScale(this.source);
        App.layoutPos.wraPos.left = x;
        App.layoutPos.wraPos.top = y;
        App.layoutPos.wraPos.width = w;
        App.layoutPos.wraPos.height = h;
        console.log("---zoom---save");
        console.log(App.layoutPos);
        App.setMouseUpTimer();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值