鼠标拖放事件

142 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情

拖放(Drag’n’Drop)是一个很赞的界面解决方案。取某件东西并将其拖放是执行许多东西的一种简单明了的方式,从复制和移动文档(如在文件管理器中)到订购(将物品放入购物车)。

在现代 HTML 标准中有一个 关于拖放的部分,其中包含了例如 dragstart 和 dragend 等特殊事件。

这些事件使我们能够支持特殊类型的拖放,例如处理从 OS 文件管理器中拖动文件,并将其拖放到浏览器窗口中。之后,JavaScript 便可以访问此类文件中的内容。

但是,原生的拖放事件也有其局限性。例如,我们无法阻止从特定区域的拖动。并且,我们无法将拖动变成“水平”或“竖直”的。还有很多其他使用它们无法完成的拖放任务。并且,移动设备对此类事件的支持非常有限。

因此,在这里我们将看到,如何使用鼠标事件来实现拖放。

拖放算法

基础的拖放算法如下所示:

  1. 在 mousedown 上 —— 根据需要准备要移动的元素(也许创建一个它的副本,向其中添加一个类或其他任何东西)。
  2. 然后在 mousemove 上,通过更改 position:absolute 情况下的 left/top 来移动它。
  3. 在 mouseup 上 —— 执行与完成的拖放相关的所有行为。

这些都是基础内容。稍后,我们将看到如何实现其他功能,例如当我们将一个东西拖动到一个元素上方时,高亮显示该元素。

下面是拖放一个球的实现代码:

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;
  };

};

如果我们运行这段代码,我们会发现一些奇怪的事情。在拖放的一开始,球“分叉”了:我们开始拖动它的“克隆”。

这是一个正在运行中的示例:

尝试使用鼠标进行拖放,你会看到这种奇怪的行为。

这是因为浏览器有自己的对图片和一些其他元素的拖放处理。它会在我们进行拖放操作时自动运行,并与我们的拖放处理产生了冲突。

禁用它:

ball.ondragstart = function() {
  return false;
};

现在一切都正常了。

这是一个正在运行中的示例:

另一个重要的方面是 —— 我们在 document 上跟踪 mousemove,而不是在 ball 上。乍一看,鼠标似乎总是在球的上方,我们可以将 mousemove 放在球上。

但正如我们所记得的那样,mousemove 会经常被触发,但不会针对每个像素都如此。因此,在快速移动鼠标后,鼠标指针可能会从球上跳转至文档中间的某个位置(甚至跳转至窗口外)。

因此,我们应该监听 document 以捕获它。

修正定位

在上述示例中,球在移动时,球的中心始终位于鼠标指针下方:

ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';

不错,但这存在副作用。要启动拖放,我们可以在球上的任意位置 mousedown。但是,如果从球的边缘“抓住”球,那么球会突然“跳转”以使球的中心位于鼠标指针下方。

如果我们能够保持元素相对于鼠标指针的初始偏移,那就更好了。

例如,我们按住球的边缘处开始拖动,那么在拖动时,鼠标指针应该保持在一开始所按住的边缘位置上。

让我们更新一下我们的算法:

  1. 当访问者按下按钮(mousedown)时 —— 我们可以在变量 shiftX/shiftY 中记住鼠标指针到球左上角的距离。我们应该在拖动时保持这个距离。

    我们可以通过坐标相减来获取这个偏移:

    // onmousedown
    let shiftX = event.clientX - ball.getBoundingClientRect().left;
    let shiftY = event.clientY - ball.getBoundingClientRect().top;
    
  2. 然后,在拖动球时,我们将鼠标指针相对于球的这个偏移也考虑在内,像这样:

    // 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;
};

In action (inside <iframe>):

如果我们按住球的右下角来进行拖动,这种差异会尤其明显。在前面的示例中,球会在鼠标指针下“跳转”一下。现在,更新后的代码可以让我们从当前位置流畅地跟随鼠标。