最近碰到了个需求,大概就是要通过可视化拖拽的方式配置一个冰柜,需要把预设好的冰柜内部架子模板一个个拖到冰箱内。一开始的想法是用鼠标事件(mousedown、mouseup等)那一套去实现,能实现但是过程过于复杂,需要控制的状态太多了。其实
Web Api
为 html 元素拖拽量身定制了一套HTML
拖放API
,用这个方法实现一些简单的拖拽功能简直不要太简单。为此写了这篇文章,下面将详细介绍HTML 拖放 API
的核心知识点
文档
一、被拖拽元素和放置被拖拽元素的元素
通常我们所了解的拖放是按住鼠标左键不放然后移动鼠标把一个页面元素从某个位置移动到另一个位置,然后松开鼠标左键,至此完成了整个拖放过程。在这个过程中我们需要先重点关注两个东西,一个是
被拖拽元素
另一个是放置被拖拽元素的元素
。
1.1 被拖拽元素
我们得先有个概念,页面上显示的元素默认并不都是可以被拖拽的(除了图片、被选中的文字、链接),所以如果当前元素默认不可被拖拽那么就得先把它设置为可拖拽的。ps:可拖拽元素被拖拽时会有一个半透明的快照跟着鼠标移动。
将 HTML 元素的 draggable 属性设置为 true, 元素就可以变为可拖拽元素。效果如下图。
<div id="box" draggable="true">draggable box</div>
1.2 可放置被拖拽元素的元素
所有的元素区域默认是不支持放置被拖拽元素的,直观的表现是,当被拖拽元素经过不可放置区域时鼠标的样式是一个禁止放置的一个图标(圆圈带一个斜杠),所以需要将目标元素设置为一个可放置区域
默认情况下是这样:
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
设置为放置区域需要给元素绑定一个事件 dragover 且要 阻止默认行为
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragover', (e) => {e.preventDefault()})
</script>
React:
放置区域
需要绑定onDragOver
事件,且要 阻止默认行为 – 其他事件一样加on
<div draggable="true">draggable box</div>
<div onDragOver={(e) => {e.preventDefault()}}>放置区域</div>
设置为可放置区域后鼠标样式也变了不再是禁止图标,而是一个加号图标(图标可以设置,下面会讲解):
然而你会发现被拖放元素并没有真正的被放置到放置区域,这是必然的,放置操作需要开发者自行定义
,以上的设置只是是为了向用户表明这个区域是允许放东西的,那么至于怎么放需要开发者自行决定。
二、拖拽过程触发的一些事件
这一小节将带你了解整个拖放过程的其他细节,比如拖拽过程中会触发哪些事件
2.1 被拖放目标触发的事件
给被拖放目标元素绑定三个事件 dragstart、drag、dragend。
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {console.log('开始拖动');})dragDom.addEventListener('drag', (e) => {console.log('拖动中');})dragDom.addEventListener('dragend', (e) => {console.log('结束拖动');})
</script>
React
<div draggable="true"onDragStart={(e) => {console.log("开始拖动", e);}}onDrag={(e) => {console.log("拖动中", e);}}onDragEnd={(e) => {console.log("结束拖动", e);}}
>
>draggable box</div>
<div onDragOver={(e) => {e.preventDefault()}}>放置区域</div>
开始拖动触发 dragstart
,拖动过程中(鼠标不松开)触发drag
,松开鼠标(或者按下 Esc
键)触发 dragend
。
2.2 被拖拽元素在放置区域内会触发的事件
先给放置目标元素绑定四个事件
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragenter', (e) => {console.log('进入到了放置区域~');})dropDom.addEventListener('dragover', (e) => {e.preventDefault()console.log('在放置区域内拖拽中~');})dropDom.addEventListener('dragleave', (e) => {console.log('离开了放置区域~');})dropDom.addEventListener('drop', (e) => {console.log('在放置区域内,放下了被拖拽元素~')})
</script>
拖拽元素进入放置区域内时触发 dragenter
事件,在放置区域内移动被拖放(鼠标不松开)元素触发 dragover
事件,被拖放元素离开放置区域触发 dragleave
事件,在放置区域内松开鼠标触发 drop
事件。
三、实现真正意义上的元素拖放
通过上面触发的事件我们可以知道,用户真正在放置区域释放鼠标的时候只有 drop 事件能够监听到。所以开发者需要在这个事件里做真正的放置操作,放置什么由开发者决定,可以是被拖拽元素,也可以是自定义的一些内容。
放置被拖拽元素:
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragover', (e) => {e.preventDefault()console.log('在放置区域内拖拽中~');})dropDom.addEventListener('drop', (e) => {console.log('在放置区域内,放下了被拖拽元素~')e.target.appendChild(document.getElementById('box'))})
</script>
放置自定义内容
dropDom.addEventListener('drop', (e) => {console.log('在放置区域内,放下了被拖拽元素~')let customCOntent = '<p>自定义内容</p>'e.target.innerHTML = e.target.innerHTML + customCOntent
})
四、dataTransfer 对象
4.1 从被拖放元素向可放置元素传递数据
dataTransfer
对象提供了一个setData()
方法,它接受两个参数,第一个参数是传递数据的类型(一般是标准的MIME类型
),第二个数据是数据值。dataTransfer
还提供了getData()
的方法用于获取传递的数据,它接受一个参数,参数值为setData
对应的第一个参数。
传递一个简单的字符串数据
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定义数据')})dropDom.addEventListener('dragover', (e) => {e.preventDefault()})dropDom.addEventListener('drop', (e) => {let data = e.dataTransfer.getData('text/plain')console.log('你传递的数据为:', data);})
</script>
⚡注意:只能在 dragstart 事件中设置数据,在其他地方设置无效。且只能在 drop 事件中获取设置的数据,其他事件中获取不到。
案例:根据传递的数据放置不同的内容。
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')let dragDom = document.getElementById('box')dropDom.addEventListener('dragover', (e) => {e.preventDefault()})dropDom.addEventListener('drop', (e) => {let num = e.dataTransfer.getData('num')console.log(num);if(num > 5)e.target.innerHTML = e.target.innerHTML + '<p>传递的数字大于5</p>'else if(num == 5) e.target.innerHTML = e.target.innerHTML + '<p>传递的数字等于5</p>'elsee.target.innerHTML = e.target.innerHTML + '<p>传递的数字小于5</p>'})dragDom.addEventListener('dragstart', (e) => {let num = Math.floor(Math.random() * 10) + 1;e.dataTransfer.setData('num', num)})
</script>
4.2 自定义拖拽过程中跟随鼠标移动的内容
默认情况下元素被拖拽时会有一个半透明的元素快照跟随着鼠标移动。通过 dataTransfer 提供的 setDragImage(elemnt, xOffset, yOffset) 方法是可以自定义跟随内容。接受三个参数 elemnt 可以是 dom 节点或者一个图片对象,xOffset, yOffset 是相对于鼠标的偏移量。
语法
dataTransfer.setDragImage(img, xOffset, yOffset);
img | Element
用于拖曳反馈图像的图像 Element 元素。
如果 Element 是一个 img 元素,则将拖动位图设置为该元素的图像(保持大小);否则,将拖动数位图设置为从给定元素所生成的图片
xOffset
使用 long 指示相对于图片的横向偏移量
yOffset
使用 long 指示相对于图片的纵向偏移量
解析
- 发生拖动时,从拖动目标 (
dragstart
事件触发的元素) 生成半透明图像,并在拖动过程中跟随鼠标指针。这个图片是自动创建的,你不需要自己去创建它。然而,如果想要设置为自定义图像,那么DataTransfer.setDragImage()
方法就能派上用场。 - 图像通常是一个
<image>
元素,但也可以是<canvas>
或任何其他图像元素。该方法的 x 和 y 坐标是图像应该相对于鼠标指针出现的偏移量。
坐标指定鼠标指针相对于图片的偏移量。例如,要使图像居中,请使用图像宽度和高度的一半。通常在dragstart
事件处理程序中调用此方法。
实际用例
setDragImage 的第一个参数接受的是一个Element参数,这样的话,普通的
html元素
、image元素
、canvas
都可以传递
1、设置为一个图片:
<script>import Tag from "../../style/imgs/attributeTag/路径.png"; //已经存在的图片let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {let img = new Image()// 创建一个图像并且使用它作为拖动图像// 请注意: 改变 "example.gif" 为一个已经存在的图片// 或者,一个还没有创建出来的图片,那么浏览器将会使用默认的拖动图片// 译者注:默认的拖动图片与拖动对象没有联系。一般是一个小型文件图标// 例如:// mg.src = Tag //或// mg.src = `data:image/png;base64,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`;img.src = 'example.gif'e.dataTransfer.setDragImage(img, 10, 10)})
</script>
2、以官网例子为例,把canvas作为参数传递,我首先尝试的是这种方式,发现并不能生效
。(官方
的例子没有
运行成功)
function dragWithCustomImage(event) {var canvas = document.createElementNS("http://www.w3.org/1999/xhtml","canvas");canvas.width = canvas.height = 50;var ctx = canvas.getContext("2d");ctx.lineWidth = 4;ctx.moveTo(0, 0);ctx.lineTo(50, 50);ctx.moveTo(0, 50);ctx.lineTo(50, 0);ctx.stroke();var dt = event.dataTransfer;dt.setData('text/plain', 'Data to Drag');dt.setDragImage(canvas, 25, 25);
}
3、根据案例,我接着使用·HtmlDivElement·作为参数传递,创建了·DIV元素·,此时也·没有生效·。
export function drawDragImage(dataTransfer: DataTransfer, context: string) {
const drawItem: API.EquipmentInfo = JSON.parse(context);
const div = document.createElement(‘div’);
div.style.height = itemObj.height + ‘px’;
div.style.width = itemObj.width + ‘px’;
div.style.border = ‘1px solid #000’;
const span = document.createElement(‘span’);
span.innerText = ‘2222’;
div.appendChild(span);
dataTransfer.setDragImage(div, drawItem.width / 2, drawItem.height / 2);
}
4、然后,我改进了canvas
,把canvas转化为图片,第一次拖拽的时候,因为image加载元素是异步导致了没有生效,如图1;第二以后拖拽的时候可以生效,如图二
。
const imageContent = canvas.toDataURL('image/jpeg', 1);const image = new Image();image.src = imageContent;image.onload = () => {console.log('image2 load');};dataTransfer.setDragImage(image, drawItem.width / 2, drawItem.height / 2);
5、最后我尝试了使用官网的方法,同样因为image加载图片是异步的
,而拖拽
事件是同步发生
的,导致了第一次执行失败
。
function dragstart_handler(ev) {console.log("dragStart");// 设置拖动的格式和数据。使用事件目标的 id 作为数据ev.dataTransfer.setData("text/plain", ev.target.id);// 创建一个图像并且使用它作为拖动图像// 请注意:改变 "example.gif" 为一个已经存在的图片// 或者,一个还没有创建出来的图片,那么浏览器将会使用默认的拖动图片// 译者注:默认的拖动图片与拖动对象没有联系。一般是一个小型文件图标var img = new Image();img.src = 'example.gif';ev.dataTransfer.setDragImage(img, 10, 10);
}
解决方案
在尝试了不同方式设置拖拽反馈图像,总结了一些解决方案
:
- 以html页面的元素为模版,动态生成内容,然后设置
Element元素参数
,可以设置DIV元素的z-index
(使用z-index,必须使用position:relative | absolute
)–(尝试使用过css1、position:absolute
定位出浏览器可视界面 2、display:none
无用),隐藏在实际页面之下:这样可以动态生成要拖拽的元素,并和生成的fabric的group保持一致。 完美的解决了问题 。
js
export function drawDragImage(dataTransfer: DataTransfer, context: string) {const drawItem: API.EquipmentInfo = JSON.parse(context);const dragElement = document.getElementById('dragItem');const idElement = dragElement?.getElementsByClassName('dragItemId')[0];const nameElement = dragElement?.getElementsByClassName('dragItemName')[0];if (idElement) {idElement.innerHTML = drawItem.id;}if (nameElement) {nameElement.innerHTML = drawItem.typeName || '';}if (dragElement) {dragElement.style.height = drawItem.height + 'px';dragElement.style.width = drawItem.width + 'px';dragElement.style.border = '1px solid #000';dragElement.style.background = '#fff';}if (dragElement) {dataTransfer.setDragImage(dragElement, drawItem.width / 2, drawItem.height / 2);}}
React
import {useRef} from "react";import { Modal, Space, Input, Tree, Button, Badge } from "antd";const mouseStyle = useRef<any>(null);<divdraggable="true"onDragStart={(e) => {//mouseStyle.currente.dataTransfer.setDragImage(mouseStyle.current, 10, 10);}}>移动位置</div>//
//absolute top-[10%] z-[1] h-10 css使用了tailwindcss
//npm install -D tailwindcss
//https://tailwindcss.com/docs/installation 文档地址<div className="absolute top-[10%] z-[1] h-10" ref={mouseStyle}><Badge count={5}><div className=" border border-[#444444] leading-6 h-6 w-15">2023.09.22初级会计资格考试</div></Badge>
</div>
4.3 设置放置前的反馈图标
dataTransfer 提供了一个
dropEffect
属性设置放置前的反馈图标,它有四种取值 none move copy link
在 dragover 中设置 dropEffect
的值
dropDom.addEventListener('dragover', (e) => {e.preventDefault()e.dataTransfer.dropEffect = 'link' // none || move || copy || link
})
- 值为 none 或者经过不可放置区域,显示禁止放置图标
- 值为 move 时
- 值为 copy 时
- 值为 link 时
4.4 拖动文件上传
通过 dataTransfer 的
files
属性可以获取用户拖拽的文件信息
拖拽系统文件到放置区域,并打印拖拽的文件信息:
dropDom.addEventListener('drop', (e) => {e.preventDefault()// 上传的文件列表let fileList = e.dataTransfer.filesfor (let i = 0; i < fileList.length; i++) {const file = fileList[i];console.log('文件名:' + file.name);console.log('文件大小:' + file.size);// 后续操作 比如:调接口上传文件}
})
4.5 清除 setData() 的值
dataTransfer 提供了
clearData()
清除 setData 设置的值,传参数则删除指定类型的值,不传则全部清除。
dropDom.addEventListener('drop', (e) => {console.log(e.dataTransfer.getData('text/plain'));console.log(e.dataTransfer.getData('text/html'));
})dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定义数据')e.dataTransfer.setData('text/html', '自定义数据2')e.dataTransfer.clearData('text/html')
})
4.6 查看设置了哪些类型的值
dataTransfer 提供了
types
属性查看 setData 设置了哪些类型的值。
dropDom.addEventListener('drop', (e) => {console.log(e.dataTransfer.types);
})
dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定义数据')e.dataTransfer.setData('text/html', '自定义数据2')
})
4.7 effectAllowed 属性取值会影响到 dropEffect 的取值效果。
effectAllowed 用于限制 dropEffect 只能设置哪些值
effectAllowed 的取值有: + none -> 此项表示 dropEffect 设置任何值都是禁止效果 + copy -> dropEffect 可以设置为 copy + copyLink -> dropEffect 可以设置为 copy 和 link + copyMove -> dropEffect 可以设置为 copy 和 Move + link -> dropEffect 可以设置为 link + linkMove -> dropEffect 可以设置为 link 和 Move + move -> dropEffect 可以设置为 Move + all -> dropEffect 可以设置为所有合法值 + uninitialized -> 等同 all 效果
dropDom.addEventListener('dragover', (e) => {e.preventDefault()e.dataTransfer.dropEffect = 'move'
})
dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.effectAllowed = 'none'
})
上面即使 dropEffect 设置为 move, 但是 effectAllowed 的值为 none,所有还是禁止放置的反馈图标。
五、总结
- 实现一个拖拽功能时先定义好被拖拽元素和放置区域元素。
- 所有的放置操作都是在 drop 事件中完成。
- 放置前的反馈效果可以根据你传递的数据来设置 dropEffect 显示不同的效果。
- 被拖拽元素也可以是放置区域,放置区域也可以是被拖拽元素,两者没有明确的界限。
- 功能自定义按需求开发