深圳幻海软件技术有限公司 欢迎您!

教你如何自定义 Drag 样式.md

2023-02-28

在web中,图片默认是可以拖拽的,通常是一个半透明的预览图(下面简称“预览图”),如下:这个预览图是系统自动生成的,一般无法进行自定义,但有时候会觉得拖拽的预览图尺寸太大了,不方便放置,例如在这种场景下。这是一个拖拽图片保存的功能,需要将图片拖拽至指定区域触发保存操作。由于拖拽时预览图太大,导致放置

在 web 中,图片默认是可以拖拽的,通常是一个半透明的预览图(下面简称“预览图”),如下:

这个预览图是系统自动生成的,一般无法进行自定义,但有时候会觉得拖拽的预览图尺寸太大了,不方便放置,例如在这种场景下。

这是一个拖拽图片保存的功能,需要将图片拖拽至指定区域触发保存操作。由于拖拽时预览图太大,导致放置区域都被遮住了,茫茫的一片,体验非常不好。

如果预览图的尺寸可以小一点,例如这样:

是不是体验就会好很多?下面一起来探讨一下。

一、预览图的默认尺寸

首先,拖拽预览图的尺寸是和页面上图片的CSS 尺寸相关联的,也就是说展示的图片越大,拖拽的预览图越大。

但是,预览图的大小并不是无限制的,据我测试,预览图的最大尺寸是 400 * 400,比如下方图片的尺寸是 500 * 500, 但是拖拽出来的预览图只有 400 * 400,但对实际开发而言,这个尺寸还是过大。

以上是 MacOS Chrome 下的表现效果,Firefox 表现要好很多,不会出现预览图过大的情况。

另外,测试了一下 Windows Chrome 的表现效果,比较类似,只不过最大尺寸是 200 * 200,表现如下:

二、借助 setDragImage 修改预览图

原生的拖拽只能通过 setDragImage[1] 来修改预览图,看着好像很强大,实则非常鸡肋,简单使用如下:

function dragstart_handler(ev) {
 var img = new Image();
 img.src = 'example.jpg';
 ev.dataTransfer.setDragImage(img, 0, 0);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这里指定了这样一张图片作为预览图:

需要绑定在 dragstart事件上,这里采用事件委托的方式。

document.addEventListener('dragstart', ev => {
  if (ev.target.tagName === 'IMG') {
    dragstart_handler(ev)
  }
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

效果是这样的:

为什么是第二次拖拽才出现自定义预览呢?这就是鸡肋的地方了,官方规定 example.jpg为一个已经存在的图片,如果这个还没有创建或者还未加载完成,那么浏览器将会使用默认的拖动图片,在第二次拖拽的时候图片加载完了,所以才生效。可以说,几乎所有setDragImage不起作用的情况都是因为这个原因。

除此之外,还有一个问题,无法直接指定预览图的大小,例如:

function dragstart_handler(ev) {
 var img = new Image();
 img.src = 'example.jpg';
 img.width = 50;//无效
 img.height = 50;
 ev.dataTransfer.setDragImage(img, 0, 0);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样也是无效的,内存中的图片会按原始大小来展示。

三、指定预览图为页面元素

其实,要想修改预览图的大小还可以通过 canvas 渲染原图,然后修改尺寸再生成图片,但是 canvas 在渲染图片时经常会出现跨域的问题,并且 canvas 也有一定的门槛,所以这里先不采用。

再回过来看看官方的语法:

dataTransfer.setDragImage(img | element, xOffset, yOffset);
  • 1.

可以看到,第一个参数不仅支持动态创建的图片,也支持页面上实际存在的元素(页面上存在的元素肯定是加载完成的)。

假设我们将自定义预览图添加在页面。

<img ondragstart="dragstart_handler" src="head.jpg">
<img id="img" src="example.jpg"><!--预览图-->
  • 1.
  • 2.

然后将预览图指定为该图片example.jpg。

ev.dataTransfer.setDragImage(img, 0, 0);
  • 1.

效果如下:

可以看到,拖拽图片就像拖拽 example.jpg 一样,而且也可以通过CSS直接修改尺寸!

#img{
  width: 100px;
  height: 100px;
}
  • 1.
  • 2.
  • 3.
  • 4.

预览图大小也是跟随 example.jpg 的。

如果把 example.jpg换成和原始图相同的链接,是不是就相当于可以自定义预览图的大小了呢?

现在需要将自定义图片隐藏起来,毕竟只是借用一下,页面上不需要显示,这里需要一点小技巧了,不能真正的将这个图片隐藏起来,比如:

#img{
  display: none;
  /*或者*/
  opacity: 0;
  /*或者*/
  visibility: none
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这些方式都不行,由于预览图是跟随这张自定义图片的,如果直接隐藏了都会导致预览图直接消失不见。

这时采取的方式可以将这张图片移到视区之外,比如这样:

#img{
  position: absolute;
  top: -9999px;
}
  • 1.
  • 2.
  • 3.
  • 4.

这样就没问题了。

实际工作中,这张图应该是自动生成的,而且自动获取当前拖拽图片的链接,完整实现就是。

/* 
* custom_drag_img
* author: xboxyan
*/
const img = document.createElement('img');
img.style = "position: absolute; top: -9999px; max-width: 100px; max-height: 100px;"
document.body.append(img)
document.addEventListener('dragstart', ev => {
  if (ev.target.tagName === 'IMG') {
    img.src = ev.target.src;
    ev.dataTransfer.setDragImage(img, 0, 0);
  }
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

可以将这段代码注入其他地方,试试效果,比如原始效果如下:

注入以上代码之后的效果如下:

在某些场景下是不是体验好很多呢?

四、自定义预览图样式

上述预览图不仅可以指定图片元素,也可以其他元素,比如div,添加一些修饰。

<div class="drag_img">
  <img src="xxx">
</div>
  • 1.
  • 2.
  • 3.
.drag_img{
  position: absolute;
  top: -9999px;
  max-width: 100px;
  max-height: 100px;
  border: 3px solid yellow;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样就可以给预览图加上一个黄色边框(强调作用)了。

亦或是通过伪元素加个角标。

.drag_img::after{
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 20px;
  height: 20px;
  background: linear-gradient(-135deg, #f44336 50%, transparent 51%);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

但是添加CSS滤镜没有生效,仍然是原始样式:

.drag_img{
  position: absolute;
  top: -9999px;
  max-width: 100px;
  max-height: 100px;
  filter: sepia(1); /*褐色滤镜*/
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这可能跟系统生成预览图的策略有关,无从得知,也无法修改

五、完全的自定义方案 draggable-polyfill

其实上述都只是针对 setDragImage做的一些自定义,本质上还是原生预览图,也仅适用于图片拖拽(但是性能超好),如果想完全自定义拖拽效果,自定义普通可拖拽元素,就必须通过 JS 模拟实现了。大致思路如下

  1. 去除默认的预览图
  2. 复制一份当前目标元素,cloneObj
  3. 监听拖拽事件,通过 transform 改变cloneObj的位置
  4. 拖拽结束移除cloneObj

详细原理可以参考我之前的一篇文章:实现一个美化原生拖拽的draggable-polyfill[2],虽然是自定义实现的,但是完全不影响原有业务拖拽逻辑,这正是 polyfill 的魅力,非常适合原生拖拽实现的场景。

项目地址:https://github.com/XboxYan/draggable-polyfill(或者访问文章底部「原文链接」)[3],非常轻量,100 来行代码,不影响业务逻辑,非常适合学习和时使用,欢迎 star~

3 年前的老代码了,最近优化了一波,非常实用,欢迎 star

六、总结一下

以上就完成了图片拖拽预览图的自定义,其实主要用于修改预览尺寸,在某些场景下还是挺有用的,这里总结一些要点:

  1. 在拖拽时产生的预览图是系统生成的,无法直接修改
  2. 在macOS Chrome下,预览图的尺寸最大为 400 * 400,在某些场景下体验不太友好
  3. setDragImage 可以修改预览图
  4. setDragImage 需要图片已经加载完成才会生效,否则仍然是默认样式
  5. setDragImage 可以指定页面上的其他元素,不仅仅是 img
  6. setDragImage 指定的元素不能真正的隐藏,否则预览图将不可见,可以通过移出到视线之外的方式
  7. 以上方案适用于图片拖拽,并且自定义的样式有限
  8. 如果需要完全自定义拖拽,可以采用 draggable-polyfill 方案,欢迎star~

总的来说,整体实现还是非常简单的,由于是原生方案,性能就不用说了,也不会影响原有功能,起到了美化的作用,如果有类似的需求,就赶紧用起来吧~

参考资料

[1]setDragImage: https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/setDragImage。

[2]实现一个美化原生拖拽的draggable-polyfill: https://segmentfault.com/a/1190000020842646。

[3]https://github.com/XboxYan/draggable-polyfill: https://github.com/XboxYan/draggable-polyfill。