项目中需要实现在主面板中,多个模块拖动布局功能,同时模块拖出主面板后变成图标展示在面板左侧,并对布局信息进行存储功能
实现思路:首先是思考拖动后相关相交模块的移动方向,这里所用模块自上而下排列,只要上边有足够的空间,则向上排列,其次左右相交的,判断拖动模块和相交模块的中心,根据中心位置,判断拖动模块是放在相交的上边还是下边;
因为频繁获取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 => {