js 实现模块拖动自由布局

js实现 多个模块拖拽布局功能,通过拖动实现模块位置调整交换,拖出面板后变成图标展示在面板左侧,并对布局信息进行存储功能实现思路:模块位置坐标化,方便计算
摘要由CSDN通过智能技术生成

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

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

因为频繁获取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 => {
                    
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值