Drag,Drop踩坑筆記
1. 幾個概念
1. 拖拽事件
摘自MDN:
HTML drag-and-drop uses the
DOM event model
and drag events inherited frommouse events
. A typical drag operation begins when a user selects a draggable element, drags the element to a droppable element, and then releases the dragged element.HTML拖拽事件使用DOM事件模型,drag事件繼承於鼠標事件
一個典型的拖拽事件從用戶選擇一個draggable元素開始,將該元素託到一個droppable元素下,然後釋放該被拖拽的元素
上述的描述中有幾個點:
- 源對象(draggable元素): 正在被拖動的對象, 該元素的draggable屬性應被設置爲true
- 目標對象: 拖拽到的目標對象,需要對元素的drag事件進行監聽,並做相應操作才能轉化爲droppable元素
2. JS拖拽事件
事件 | 事件處理函數 | 作用對象 | 描述 |
---|---|---|---|
drag | ondrag | 源對象 | 源文件被拖動觸發 |
dragstart | ondragstart | 源對象 | 用戶開始拖拽源對象 |
dragend | ondragend | 源對象 | 用戶結束拖拽操作(例如釋放鼠標按鍵和點擊ESC按鍵) |
dragenter | ondragenter | 目標對象 | 拖拽源對象進入目標對象 |
dragover | ondragover | 目標對象 | 源對象處於目標對象上方(每幾百毫秒觸發一次) |
dragleave | ondragleave | 目標對象 | 源對象離開目標對象區域 |
dragexit | ondragexit | 目標對象 | 元素不再爲可被選擇的目標對象 |
drop | ondrop | 目標對象 | 源對象落在目標對象上 |
1. 幾個注意點
- 關於dragenter和dragover的注意點
摘自mdn:
A listener for the
dragenter
anddragover
events are used to indicate valid drop targets, that is, places where dragged items may be dropped. Most areas of a web page or application are not valid places to drop data. Thus, the default handling of these events is not to allow a drop.If you want to allow a drop, you must prevent the default handling by cancelling the event. You can do this either by returning
false
from an attribute-defined event listener, or by calling the event’spreventDefault()
method. The latter may be more feasible in a function defined in a separate script.
根據以上描述歸納三點:
- 通過監聽一個元素的dragenter和dragover事件可以表明一個元素是有效的目標對象
- 如果執行上述兩個事件的默認處理函數,源對象還是不能被drop的
- 如果想要被允許drop的話,需要通過preventDefault來該事件的默認行爲
2. 拖動案例
圖片拖動案例參考: 原生JS快速實現拖放(drag and drop)效果
- DOM結構
<body>
<div class="droppable">
<div class="box" draggable="true"></div>
</div>
<div class="droppable"></div>
<div class="droppable"></div>
<div class="droppable"></div>
<div class="droppable"></div>
<div class="droppable"></div>
</body>
- JS實現圖片的拖拽
主要思路:
- draggale元素本身在拖拽的時候,文件不會消失,因此需要手動添加一個display:none使原來的元素不顯示
- 對droppable的落點框進行dragenter, dragover事件的監聽,這裏要阻止默認的事件的處理函數
- 對drop事件的回調進行操作,當事件落下的時候進行操作
const dragElem = document.querySelector('.box');
const droppables = document.querySelectorAll('.droppable');
dragElem.addEventListener('drag', function(event) {
console.log('-------------Drag Event--------------');
});
dragElem.addEventListener('dragstart', function() {
console.log('-------------Drag Start Event--------------');
setTimeout(() => {
this.classList.add('invisible');
}, 0);
});
dragElem.addEventListener('dragend', function() {
console.log('-------------Drag End Event--------------');
setTimeout(() => {
this.classList.remove('invisible');
}, 0);
});
droppables.forEach((elem, index) => {
elem.addEventListener('dragenter', function(event) {
// 如果不調用event.preventDefault,導致drop事件失效
event.preventDefault();
console.log(`Droppable ${index} dropover event`);
});
elem.addEventListener('dragover', function(event) {
// dropover事件的默認處理函數會使得drop事件不被捕獲
event.preventDefault();
console.log(`Droppable ${index} dropover event`);
this.classList.add('drag-over');
});
elem.addEventListener('dragleave', function(event) {
event.preventDefault();
console.log(`Dragleave ${index} dropover event`);
this.classList.remove('drag-over')
});
elem.addEventListener('drop', function(event) {
console.log(`Drop in ${index}`);
setTimeout(() => {
this.append(dragElem);
dragElem.classList.remove('invisible');
this.classList.remove('drag-over')
}, 0);
});
});
3. 幾個注意點
dataTransfer中保存的爲字符串,因此我們需要將獲取到的字符串先轉換成string,然後再轉換爲dom節點進行添加
4. 代碼實現
- dom節點與string相互轉換的一個工具類
const HtmlStringTransfer = function() {
this.secret = null;
this.setSecret = function() {
const _secret = Math.random().toString(36).substr(2);
this.secret = _secret;
}
this.getString = function(HTMLNode) {
this.setSecret();
HTMLNode.setAttribute('id', this.secret);
return HTMLNode.outerHTML;
};
this.getNode = function() {
const _node = document.getElementById(this.secret);
_node.removeAttribute('id');
return _node;
};
};
- 利用dataTransfer.setData設置源對象
const Factory = new HtmlStringTransfer();
dragElem.addEventListener('dragstart', function(event) {
console.log('-------------Drag Start Event--------------');
const _sourceElement = Factory.getString(event.target);
event.dataTransfer.setData('sourceElement', _sourceElement);
setTimeout(() => {
this.classList.add('invisible');
}, 0);
});
- 利用dataTransfer.getData在drop時獲得源對象
elem.addEventListener('drop', function(event) {
console.log(`Drop in ${index}`);
const sourceElement = event.dataTransfer.getData('SourceElement');
const _sourceElement = Factory.getNode(sourceElement);
setTimeout(() => {
this.append(_sourceElement);
dragElem.classList.remove('invisible');
this.classList.remove('drag-over');
}, 0);
});
- 完整代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Test</title>
<style>
body {
background: #eee;
}
.droppable {
display: inline-block;
width: 160px;
height: 160px;
margin: 10px;
border: 3px salmon solid;
background-color: #fff;
margin: 20px;
}
.box {
width: 150px;
height: 150px;
background-image: url('./1.jpg');
background-size: 150px 150px;
}
.drag-over {
border-style: dashed;
}
.dragging {
background-color: yellow;
}
.invisible {
display: none;
}
</style>
</head>
<body>
<div class="droppable" src-index="0">
<div draggable="true" class="box"></div>
</div>
<div class="droppable" src-index="1"></div>
<div class="droppable" src-index="2"></div>
<div class="droppable" src-index="3"></div>
<div class="droppable" src-index="4"></div>
<div class="droppable" src-index="5"></div>
<script>
const dragElem = document.querySelector('.box');
const droppables = document.querySelectorAll('.droppable');
const HtmlStringTransfer = function() {
this.secret = null;
this.setSecret = function() {
const _secret = Math.random().toString(36).substr(2);
this.secret = _secret;
}
this.getString = function(HTMLNode) {
this.setSecret();
HTMLNode.setAttribute('id', this.secret);
return HTMLNode.outerHTML;
};
this.getNode = function() {
const _node = document.getElementById(this.secret);
_node.removeAttribute('id');
return _node;
};
};
const Factory = new HtmlStringTransfer();
dragElem.addEventListener('drag', function(event) {
console.log('-------------Drag Event--------------');
});
dragElem.addEventListener('dragstart', function(event) {
console.log('-------------Drag Start Event--------------');
const _sourceElement = Factory.getString(event.target);
event.dataTransfer.setData('sourceElement', _sourceElement);
setTimeout(() => {
this.classList.add('invisible');
}, 0);
});
dragElem.addEventListener('dragend', function() {
console.log('-------------Drag End Event--------------');
setTimeout(() => {
this.classList.remove('invisible');
}, 0);
});
droppables.forEach((elem, index) => {
elem.addEventListener('dragenter', function(event) {
// 如果不調用event.preventDefault,導致drop事件失效
event.preventDefault();
console.log(`Droppable ${index} dropover event`);
});
elem.addEventListener('dragover', function(event) {
// dropover事件的默認處理函數會使得drop事件不被捕獲
event.preventDefault();
console.log(`Droppable ${index} dropover event`);
this.classList.add('drag-over');
});
elem.addEventListener('dragleave', function(event) {
event.preventDefault();
console.log(`Dragleave ${index} dropover event`);
this.classList.remove('drag-over');
});
elem.addEventListener('drop', function(event) {
console.log(`Drop in ${index}`);
const sourceElement = event.dataTransfer.getData('SourceElement');
const _sourceElement = Factory.getNode(sourceElement);
setTimeout(() => {
this.append(_sourceElement);
dragElem.classList.remove('invisible');
this.classList.remove('drag-over');
}, 0);
});
});
</script>
</body>
</html>
4. 實驗結果分析
1. 實驗結果
2. 事件的監聽順序
- 源對象的drag start
- 源對象的drag事件(只要鼠標拖動就會產生該事件)
- 拖動後第一次產生源對象所在目標對象的dragenter事件
- 源對象所在的對象的dragover事件
- 離開原目標對象的dragleave事件
- 新目標對象的dragenter事件
- 新目標對象的dragover事件
- drop時新目標對象的drop事件