背景
最近产品提了个需求,要实现往画布拖拽图片、svg,在同类组件上松开鼠标时进行替换操作,反之进行新增。
最终效果如下:
功能分析
要实现本功能,可以简单拆解成以下几点:
- 要拖拽元素区域绑定鼠标事件,进行事件监听,设置拖拽状态
- 拖动元素时,生成临时元素跟随鼠标,并实时设置元素样式
- 松开鼠标后,获取“落脚点”处元素,判断是新增还是替换元素,最后清空临时元素
实现流程
1、绑定事件
-
首先给拖拽区域绑定
mousedown事件,建议将事件绑定到可拖拽元素的公共父容器上,利用事件委派的机制判断点击的元素,如果给每一个元素单独绑定事件会消耗更多内存。 -
mousemove和mouseup绑定到document上,这个没啥好说的(详细逻辑后续讲解)
假如拖拽区域的dom结构如下,我们可以将事件绑定到
list-wrap上
<!-- 素材拖动区域 -->
<div class="list-wrap left">
<div class="tit">素材区</div>
<div class="drag-wrap">
<img class="drag-ele" src="xxx" />
<img class="drag-ele" src="xxx" />
...
</div>
</div>
2、事件处理
mousedown事件
我们在开始应该给可拖拽元素增加一个通用类名用于标识,这边我们用drag-ele去做标识。鼠标按下时,我们判断当前元素如果存在此类名,则设置拖拽状态dragState并且记录拖拽元素,用于后续逻辑处理。
// 拖拽状态
let dragState = false;
// 拖拽元素
let dragEle = null;
function mouseDown(event) {
// 判断在可拖动元素上进行点击时,才执行后续拖拽相关逻辑
if (event.target.classList.contains('drag-ele')) {
dragState = true;
dragEle = event.target;
}
}
mousemove事件
在此事件内,我们需要创建跟随鼠标的临时元素,并实时设置元素的位置。
创建元素
创建时首先判断是否已存在临时元素,存在的话则跳过创建流程,反之进行元素创建。
为了使创建的临时元素与鼠标在元素上按下时,保持相同的视觉位置效果,需要对其left和top做一些处理,使用鼠标的当前的位置减去鼠标相对于元素的偏移位置,才能得到正确的left和top。
left = clientX - offsetX;
top = clientY - offsetY;
拖动时,元素放大效果实现
通常拖拽区域的的缩略图比较小,为了易用性,我们拖拽时,可以逐步放大图片,让用户看的更清晰些。
效果如下:
实现这个效果需要事先设定几个参数:
最大放大倍数 - 拖拽元素最大放大的倍数
最大移动距离 - 拖动多少距离能达到最大倍数
上图中最大放大倍数是2,最大移动距离是100,以上两个值,可以根据项目情况随便调整。
在首次生成临时元素时,记录鼠标的初始位置和相对元素的偏移,然后在鼠标拖动的过程中,判断鼠标的移动距离,计算要放大的倍数,进行设置。
优化:设置缩放时有一个需要注意的点,scale默认是基于元素中心进行的,不做任何处理,元素放大时会导致鼠标视觉位置的偏移,比如下图,我在索隆的手上拖动,随着放大,位置发生了偏移。
要解决这个问题,需要在初始的时候设置下transform-origin(设置动画基点),设置值为鼠标初始相对于图片的偏移,这样放大后就能保持视觉统一了。
代码参考:
// 存储创建的临时元素
let dragTempEle = null;
function mouseMove(event) {
if (dragState) {
// 创建临时元素
createTempEle(event);
// 修改临时元素位置
setTempPosi(event);
}
}
// 拖拽元素时生成临时元素
function createTempEle(event) {
// 判断是否已创建临时元素
if (dragTempEle) return;
const w = event.target.offsetWidth;
const h = event.target.offsetHeight;
let left = 0;
let top = 0;
dragEleInfo = {
offX: event.offsetX,
offY: event.offsetY,
left: event.clientX,
top: event.clientY
};
let tempEle = event.target.cloneNode(true);
tempEle.id = 'drag-temp';
// 获取拖拽元素内的封图元素,克隆的元素内使用原图,保证放大后的清晰度
tempEle.style.cssText = `max-width:${w}px; max-height:${h}px;`;
// 设置临时元素的样式
left = event.clientX - dragEleInfo.offX;
top = event.clientY - dragEleInfo.offY;
tempEle.style.cssText = `
position: fixed;
left: ${left}px;
top: ${top}px;
z-index:100;
width:${w}px;
height:${h}px;
opacity:.6;
transform-origin:${dragEleInfo.offX}px ${dragEleInfo.offY}px;
`;
dragTempEle = tempEle;
document.body.appendChild(tempEle);
}
// 拖动时,设置临时元素的位置
function setTempPosi(event) {
if (dragState) {
// 计算移位置
dragTempEle.style.left = event.clientX - dragEleInfo.offX + 'px';
dragTempEle.style.top = event.clientY - dragEleInfo.offY + 'px';
// 根据横向拖动距离,计算放大比例(最多放大2倍)
const moveX = event.clientX - dragEleInfo.left;
const ratio = Math.min(Math.max((moveX / 100) * 1, 0), 1);
const scale = 1 + ratio;
dragTempEle.style.transform = `scale(${scale})`;
}
}
mouseup事件
此事件内需要做两件事,判断是新增还是替换元素、重置拖拽状态清空临时数据。
判断新增还是替换,需要判断鼠标松开时,当前位置的元素,如果是在放置区图片上松开,则进行替换,反之则是新增。
要获取松开位置的元素列表,可以通过document.elementsFromPoint(x,y)获取,然后遍历元素,根据标识判断即可。
替换:直接替换图片地址即可,无需赘述
新增:需要根据放置区和鼠标位置计算出元素最终相对于容器的位置,如下图
元素最终的 left = left1 - left2 - 初始鼠标相对于元素的左偏移 , top同理
优化:如果是在一些低代码编辑器中做这个功能,因为编辑器的画布通常带有缩放功能。在新增元素时,为了保持拖拽元素的视觉效果和新增的元素一致,需要对新增元素尺寸做转换处理,转换的逻辑与画布的缩放比例成反比,画布放的越大,元素尺寸转换结果越小,画布缩放越小,元素尺寸转换结果越大,借此达到视觉统一。
代码参考:
function mouseUp(event) {
if (dragState) {
// 判断是否在同类元素上松开,是则进行替换,反之新增
const sameEle = findSameEle();
if (sameEle) {
replaceEle(sameEle);
} else {
addEle();
}
// 移除拖拽临时元素,重置拖拽相关数据
dragState = false;
dragTempEle = null;
if ($('#drag-temp')) {
$('#drag-temp').remove();
}
}
}
// 查找当前位置是否所有同类元素
function findSameEle() {
const eles = document.elementsFromPoint(event.pageX, event.pageY);
let sameEle = null;
for (let item of eles) {
if (item.classList.contains('eles')) {
sameEle = item;
break;
}
}
return sameEle;
}
// 拖拽新增数据
function addEle(e) {
const eles = document.elementsFromPoint(event.pageX, event.pageY);
// 判断在画布上松开鼠标才新增元素
let add = false;
for (let ele of eles) {
// 如果在放置区松开鼠标则新增
if (ele.classList.contains('place-wrap')) {
add = true;
break;
}
}
if (add) {
// 获取画布和临时元素信息
const place = $('.place-wrap');
const info = place.getBoundingClientRect();
const infoTemp = dragTempEle.getBoundingClientRect();
// 计算元素插入的位置、尺寸等信息
const imgEle = document.createElement('img');
imgEle.src = dragTempEle.src;
imgEle.classList.add('eles');
imgEle.style.cssText = `
position: absolute;
left: ${infoTemp.x - info.x}px;
top: ${infoTemp.y - info.y}px;
width:${infoTemp.width}px;
height:${infoTemp.height}px;
`;
place.appendChild(imgEle);
}
}
// 松开鼠标判断新增还是替换同类元素
function replaceEle(sameEle) {
sameEle.src = dragTempEle.src;
}