一、拖放是一个很赞的界面解决方案。
二、拖放使用场景:复制、移动文档(如在文档管理器中)、订购(将物品放入购物车)。
三、现代HTML标准中的拖放,包含了如dragstart、dragend等特殊事件。
1、局限性:
(1)无法阻止从特定区域的拖动。
(2)无法将拖动变成“水平”或“竖直的”。
(3)移动设备对此类事件的支持非常有限。

用鼠标事件来实现拖放

拖放算法

一、基础的拖放算法
1、在mousedown上:根据需要准备要移动的元素(也许创建一个它的副本,向其中添加一个类或其他任何东西)。
2、在mousemove上,通过更改position: absolute情况下的left / top 来移动它。
3、在mouseup上,执行与完成的拖放相关的所有行为。
二、将一个东西拖动到一个元素上方时,高亮显示该元素。
【示例1】拖放一个球的实现代码

  1. ball.onmousedown = function(event) {
  2. // (1) 准备移动:确保 absolute,并通过设置 z-index 以确保球在顶部
  3. ball.style.position = 'absolute';
  4. ball.style.zIndex = 1000;
  5. // 将其从当前父元素中直接移动到 body 中
  6. // 以使其定位是相对于 body 的
  7. document.body.append(ball);
  8. // 现在球的中心在 (pageX, pageY) 坐标上
  9. function moveAt(pageX, pageY) {
  10. ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
  11. ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
  12. }
  13. // 将我们绝对定位的球移到指针下方
  14. moveAt(event.pageX, event.pageY);
  15. function onMouseMove(event) {
  16. moveAt(event.pageX, event.pageY);
  17. }
  18. // (2) 在 mousemove 事件上移动球
  19. document.addEventListener('mousemove', onMouseMove);
  20. // (3) 放下球,并移除不需要的处理程序
  21. ball.onmouseup = function() {
  22. document.removeEventListener('mousemove', onMouseMove);
  23. ball.onmouseup = null;
  24. };
  25. };

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)期望:我们按住球的边缘处开始拖动,那么在拖动时,鼠标指针应该保持在一开始所按住的边缘位置上。
image.png
(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。