一、拖放是一个很赞的界面解决方案。
二、拖放使用场景:复制、移动文档(如在文档管理器中)、订购(将物品放入购物车)。
三、现代HTML标准中的拖放,包含了如dragstart、dragend等特殊事件。
1、局限性:
(1)无法阻止从特定区域的拖动。
(2)无法将拖动变成“水平”或“竖直的”。
(3)移动设备对此类事件的支持非常有限。
用鼠标事件来实现拖放
拖放算法
一、基础的拖放算法
1、在mousedown上:根据需要准备要移动的元素(也许创建一个它的副本,向其中添加一个类或其他任何东西)。
2、在mousemove上,通过更改position: absolute情况下的left / top 来移动它。
3、在mouseup上,执行与完成的拖放相关的所有行为。
二、将一个东西拖动到一个元素上方时,高亮显示该元素。
【示例1】拖放一个球的实现代码
ball.onmousedown = function(event) {
// (1) 准备移动:确保 absolute,并通过设置 z-index 以确保球在顶部
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
// 将其从当前父元素中直接移动到 body 中
// 以使其定位是相对于 body 的
document.body.append(ball);
// 现在球的中心在 (pageX, pageY) 坐标上
function moveAt(pageX, pageY) {
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
}
// 将我们绝对定位的球移到指针下方
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// (2) 在 mousemove 事件上移动球
document.addEventListener('mousemove', onMouseMove);
// (3) 放下球,并移除不需要的处理程序
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
1、问题:在拖放的一开始,球“分叉”了:我们开始拖动它的“克隆”。
(1)原因:浏览器有自己的对图片和一些其他元素的拖放处理。它会在我们进行拖放操作时自动运行,并与我们的拖放处理产生了冲突。
(2)解决:禁用浏览器默认行为
ball.ondragstart = function() {
return false;
}
2、问题:在快速移动鼠标后,鼠标指针可能会从球上跳转至文档中间的某个位置(甚至跳转至窗口外)。
(1)原因:我们在document上跟踪mousemove,而不是在ball上。乍一看,鼠标似乎总是在球的上方,我们可以将mousemove放在球上。实际上,mosemove会经常被触发,但不会针对每个像素都如此。因此,
(2)解决:我们应该监听document以捕获它。
修正定位
一、在上述示例中,球在移动时,球的中心始终位于鼠标指针下方
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
1、副作用:要启动拖放,我们可以在球上的任意位置mousedown。但是,如果从球的边缘“抓住”球,那么球会突然“跳转”以使球的中心位于鼠标指针下方。
(1)如果我们能够保持元素相对于鼠标指针的初始偏移,那就更好了。
(2)期望:我们按住球的边缘处开始拖动,那么在拖动时,鼠标指针应该保持在一开始所按住的边缘位置上。
(3)解决:更新算法
①当访问者按下按钮时(mousedown)时:我们在变量shiftX / shiftY中记住鼠标指针到球左上角的距离。我们应该在拖放时保持这个距离。
a. 我们可以通过坐标相减来获取这个偏移
// onmousedown
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
②然后,在拖动球时,我们将鼠标指针相对于球的这个偏移也考虑在内
// onmousemove
// 球具有position:absolute
ball.style.left = event.pageX - shiftX + 'px';
ball.style.top = event.pageY = shiftY + 'px';
二、最终代码
ball.onmousedown = function(event) {
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
moveAt(event.pageX, event.pageY);
// 移动现在位于坐标 (pageX, pageY) 上的球
// 将初始的偏移考虑在内
function moveAt(pageX, pageY) {
ball.style.left = pageX - shiftX + 'px';
ball.style.top = pageY - shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// 在 mousemove 事件上移动球
document.addEventListener('mousemove', onMouseMove);
// 放下球,并移除不需要的处理程序
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
ball.ondragstart = function() {
return false;
};
潜在的放置目标
一、前面的示例中,球可以被放置(drop)到“任何地方”。在实际上,我们通常是将一个元素放到另一个元素上。如将一个“文件”放置到一个“文件夹”或者其他地方。
二、抽象地讲,我们取一个“draggable”的元素,并将其放在“droppable”的元素上。
1、期望:
(1)我们在拖放结束时,所拖动的元素要放在哪里-执行相应的行为
(2)并且,最好知道我们所拖动到的“droppable”的元素的位置,并高亮显示“droppable”的元素。
2、解决方案1:
(1)将mouseover / mouseup处理程序放在潜在的“droppable”的元素中。
(2)结论:不可行。
(3)原因:当我们拖动时,可拖动元素一直是位于其他元素上的。而鼠标事件只发生在顶部元素上,而不是发生在那些下面的元素。球始终位于其他元素之上,因此事件会发生在球上。无论我们在较低的元素上设置什么处理程序,它们都不会起作用。
3、解决方案2:有一个叫做document.elementFromPoint(clienX, clientY)的方法。它会返回在给定的窗口相对坐标处的嵌套的最深的元素(如果给定的坐标在窗口外,则返回null)。我们可以再我们的任何鼠标事件处理程序中使用它,以检测鼠标指针下的潜在的”droppable”的元素。
(1)我们可以使用下面的代码来检查我们正在“飞过”的元素是什么。并在放置(drop)时,对放置进行处理。
// 在一个鼠标事件处理程序中
ball.hidden = true; // 隐藏我们拖动的元素。
// 我们需要隐藏球。否则,我们通常会在这些坐标上有一个球,因为它是在鼠标指针下的最顶部的元素:elemBelow = ball
let elemBelow = document.elementFromPoint(event.clientX, event.clientY); // elemBelow是球下方的元素,可能是droppable的元素
ball.hidden = false;
(2)基于onMouseMove扩展的代码,用于查找“droppable”的元素
// 我们当前正在飞过的潜在的 droppable 的元素
let currentDroppable = null;
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
// mousemove 事件可能会在窗口外被触发(当球被拖出屏幕时)
// 如果 clientX/clientY 在窗口外,那么 elementfromPoint 会返回 null
if (!elemBelow) return;
// 潜在的 droppable 的元素被使用 "droppable" 类进行标记(也可以是其他逻辑)
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable != droppableBelow) {
// 我们正在飞入或飞出...
// 注意:它们两个的值都可能为 null
// currentDroppable=null —— 如果我们在此事件之前,鼠标指针不是在一个 droppable 的元素上(例如空白处)
// droppableBelow=null —— 如果现在,在当前事件中,我们的鼠标指针不是在一个 droppable 的元素上
if (currentDroppable) {
// 处理“飞出” droppable 的元素时的处理逻辑(移除高亮)
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) {
// 处理“飞入” droppable 的元素时的逻辑
enterDroppable(currentDroppable);
}
}
}
总结
一、关键部分
1、事件流:ball.mousedown -> document.mousemove -> ball.mouseup(不要忘记取消原生ondragstart)
2、在拖动开始时,记住鼠标指针相对于元素的初始偏移(shift):shiftX / shiftY,并在拖动过程中保持它不变。
3、使用docuemt.elementFromPoint检测鼠标指针下的“droppable”的元素。
二、我们可以在此基础上做很多事情
1、在mouseup上,我们可以智能地完成放置(drop):更改数据,移动元素。
2、我们可以高亮我们正在“飞过”的元素。
3、我们可以将拖动限制在特定的区域或者方向。
4、我们可以对mousedown / up使用事件委托。一个大范围的用于检查event.target的事件处理程序可以管理数百个元素的拖放。
三、在此基础上已经将体系结构构建好的框架:DragZone, Droppable, Draggable及其他class。