引言
在项目中有一个需求是给一个左侧的滚动列表的每一个列表元素加一个拖拽属性。实际实现中遇到了一个问题:列表的高度是固定的,而列表元素有很多项,因此列表是允许滚动的。此时如果某一个列表元素恰好处在了列表边缘的位置,那么其有一部分是被遮挡掉的,而拖拽该元素时,生成的拖拽元素也是不完整的。
一、问题复现
先在HTML中加入一些列表元素
<ul class="input-list" id="input-list">
<li class="input-item" draggable="true"><span>1</span></li>
...
<li class="input-item" draggable="true"><span>9</span></li>
</ul>
给外层 ul 元素设置一个固定高度,给内部的列表元素 li 设置一个较小的固定高度,使这些列表元素的高度之和大于 ul 的高度。可以给它们设置一些背景颜色以便于观察。
此时在Chrome中打开这个网页,可以看到问题出现
同样的网页在Firefox中打开不存在上述问题
二、解决方案
尝试使用 html2canvas 生成拖拽节点的图片,但是无论是使用 Base64 还是 BlobUrl 都无法渲染图片,遂放弃。
参考 vue项目中在使用draggable进行拖拽时,超出滚动条外的,那么拖拽的元素将会不完整显示 一文中的方案,可以先将要拖拽的节点复制一份,然后作为dataTransfer.setDragImage()的第一个参数使用。注意生成的克隆节点是不具有原节点样式的,因此要手动添加。
let el = event.target;
this.ghost = el.cloneNode(true);
this.ghost.className = el.className;
// 复制样式
this.ghost.style.width = ...
document.body.appendChild(ghost);
// 下面两行代码的作用是防止新增的元素破坏页面结构
document.body.lastChild.style.position = 'absolute';
document.body.lastChild.style.left = '-100%';
event.dataTransfer.setDragImage(this.ghost, 0, 0);
也可以通过复刻列表元素在原始 DOM 中的层级位置,通过其外层元素的类名控制其样式。例如这里可以添加一个 ul 列表,列表项仅包含被拖拽的 li 元素。
...
const ul = document.createElement('ul');
ul.className = 'input-list';
ul.appendChild(this.ghost);
document.body.appendChild(ul);
...
完整代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Draggable</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
}
.input-list {
position: relative;
width: 250px;
height: 300px;
overflow: auto;
cursor: pointer;
list-style: none;
}
.input-list > .input-item > span {
display: inline-block;
border: 1px solid #838282;
background-color: #a0e1ee;
height: 50px;
width: 100%;
text-align: center;
font-size: larger;
font-weight: 900;
line-height: 50px;
color: red;
pointer-events: none;
}
</style>
</head>
<body>
<ul class="input-list" id="input-list">
<li class="input-item" draggable="true"><span>1</span></li>
<li class="input-item" draggable="true"><span>2</span></li>
<li class="input-item" draggable="true"><span>3</span></li>
<li class="input-item" draggable="true"><span>4</span></li>
<li class="input-item" draggable="true"><span>5</span></li>
<li class="input-item" draggable="true"><span>6</span></li>
<li class="input-item" draggable="true"><span>7</span></li>
<li class="input-item" draggable="true"><span>8</span></li>
<li class="input-item" draggable="true"><span>9</span></li>
</ul>
</body>
<script>
const inputItems = document.querySelectorAll('.input-item');
inputItems.forEach((item) => {
item.addEventListener(
'dragstart',
function (event) {
let el = event.target;
this.ghost = el.cloneNode(true);
this.ghost.className = el.className;
const ul = document.createElement('ul');
ul.className = 'input-list';
ul.appendChild(this.ghost);
document.body.appendChild(ul);
document.body.lastChild.style.position = 'absolute';
document.body.lastChild.style.left = '-100%';
event.dataTransfer.setDragImage(this.ghost, 50, 25);
},
false
);
item.addEventListener(
'dragend',
function (event) {
document.body.removeChild(this.ghost);
},
false
);
});
</script>
</html>
修改后拖拽被遮挡的列表项能够完整显示
三、其他方法
如果在 Vue 项目中使用 vue-drag-drop 实现拖拽,可以利用该组件中的 image API,将要拖拽的元素节点复制一份通过 v-html 插入 slot 中,原理基本与原生方案相同。