任务流程版
-- 拖拽清单的实战应用
本课程主要讲解 JavaScript 拖拽事件。在日常开发中,我们会涉及到拖拽排序、拖拉等的交互情景,就是通过拖拽事件来完成。课程概括了元素拖拽相关的知识点,通过本课程,完成一个拖拽清单的实战应用应用,大家将对 web 页面中拖拽原理和开发有更深入的了解。
主要包含以下功能:
- 在当前任务列表拖拽排序
- 状态列表之间可拖拽切换
- 拖拽到垃圾桶出删除项目
原理
HTML5 的拖放 API 能够支持在网站内部和网站之间拖放项目。同时也提供了一个更简单的供扩展和基于 Mozilla 的应用程序使用的 API。一个典型的drag操作是这样开始的:
- 用户用鼠标选中一个可拖动的(draggable)元素
- 移动鼠标到一个可放置的(droppable)元素
- 然后释放鼠标。
知识点
- draggable 属性 想要拖放某个元素,必须设置该元素的 draggable 属性为 true,当该属性为 false 时,将不允许拖放。而 img 元素和 a 元素都默认设置了 draggable 属性为 true,可直接拖放,如果不想拖放这两个元素,把属性设为 false 即可。
<div class="item" draggable="true">可以拖拽元素</div>
放置区域 当拖动一个项目到HTML元素中时,浏览器默认不会有任何响应。想要让一个元素变成可释放区域,该元素必须设置 dragover 和 drop 事件处理程序属性。dragover 处理程序需调用 preventDefault() 来阻止对这个事件的其它处理过程(如触点事件或指针事件)。
相关事件
- 源对象:
- dragstart:源对象开始拖放。
- drag:源对象拖放过程中。
- dragend:源对象拖放结束。
- 过程对象:
- dragenter:源对象开始进入过程对象范围内。
- dragover:源对象在过程对象范围内移动。
- dragleave:源对象离开过程对象的范围。
- 目标对象:
- drop:源对象被拖放到目标对象内。
实现步骤
- HTML & CSS
- 构建单例对象PAGE
- 实现在当前任务列表拖拽排序
- 实现拖拽到垃圾桶出删除项目
HTML & CSS
- html
<div class="drag-container" id="drag-container">
<div class="doing-section">
<p class="sub-title">进行中</p>
<div class="drag-list">
<div class="drag-item" draggable="true" data-id="1">1</div>
<div class="drag-item" draggable="true" data-id="2">2</div>
<div class="drag-item" draggable="true" data-id="3">3</div>
</div>
</div>
<div class="done-section">
<p class="sub-title">已完成</p>
<div class="drag-list">
<div class="drag-item" draggable="true" data-id="4">4</div>
<div class="drag-item" draggable="true" data-id="5">5</div>
<div class="drag-item" draggable="true" data-id="6">6</div>
</div>
</div>
<div class="delete-section">
<p class="sub-title">回收站</p>
</div>
</div>
- CSS
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,body{
height: 100%;
}
.drag-container{
height: 100%;
padding: 10px;
display: flex;
}
.doing-section,
.done-section,
.delete-section{
display: flex;
flex-direction: column;
width: 288px;
margin-right: 10px;
background-color: #eee;
border-radius: 3px;
padding: 10px;
}
.sub-title{
padding: 14px 18px;
font-size: 15px;
font-weight: 700;
}
.drag-list{
flex: auto;
}
.drag-item{
background: #fff;
border-radius: 8px;
margin-bottom: 10px;
padding: 12px 10px 12px 20px;
line-height: 20px;
overflow: hidden;
}
.drag-item:hover{
transition: all .2s ease;
border-left: 10px solid #ddd;
cursor: pointer;
}
.drag-item.draging{
background: #f5f5f5;
opacity: .5;
}
构建单例对象PAGE
- 定义 PAGE
- 定义 PAGE.data 方法
- 定义 draging_id、draging_el、draging_pr 拖拽元素 id 、拖拽元素、以及当前的拖拽元素的父级元素
- 定义 PAGE.init、PAGE.bind ,并 在PAGE.init 中调用 PAGE.bind
- 定义 PAGE.onEventLister 委托绑定方法
- 调用 PAGE.init
// 1. 定义 PAGE
const PAGE = {
// 2. 定义 PAGE.data 方法
data: function() {
// 3. 定义 draging_id、draging_el、draging_pr 拖拽元素 id 、拖拽元素、以及当前的拖拽元素的父级元素
draging_id: null,
draging_el: null
},
// 4. 定义 PAGE.init、PAGE.bind ,并 在PAGE.init 中调用 PAGE.bind
init: function() {
this.bind();
},
bind: function() {
},
// 5. 定义 PAGE.onEventLister 委托绑定方法
onEventLister: function(parentNode,action,childClassName,callback) {
parentNode.addEventListener(action,function(e){
e.target.className.indexOf(childClassName) >= 0 && callback(e);
})
},
}
// 6. 调用 PAGE.init
PAGE.init();
实现在当前任务列表拖拽排序
- 为项目委托绑定拖拽开始、拖拽进入、拖拽结束事件
- 拖拽开始时,记录当前拖拽元素的 id、元素、父级元素,并修改 className
- 拖拽移动时,拖拽移动时,根据鼠标位置与当前项目的位置放置拖拽元素
- 拖拽结束时,清除保存的 PAGE.data 拖拽内容,并还原 className
const PAGE = {
data: function() {
draging_id: null,
draging_el: null
},
init: function() {
this.bind();
},
bind: function() {
// 1. 为项目委托绑定拖拽开始、拖拽进入、拖拽结束事件
let dragContainer = document.getElementById('drag-container');
this.onEventLister(dragContainer,'dragstart','drag-item',this.itemDragStart);
this.onEventLister(dragContainer,'dragover','drag-item',this.itemDragOver);
this.onEventLister(dragContainer,'dragend','drag-item',this.itemDragEnd);
},
onEventLister: function(parentNode,action,childClassName,callback) {
parentNode.addEventListener(action,function(e){
e.target.className.indexOf(childClassName) >= 0 && callback(e);
})
},
// 2. 拖拽开始时,记录当前拖拽元素的 id、元素、父级元素,并修改 className
itemDragStart: function(e){
let id = e.target.dataset.id;
PAGE.data.draging_id = id;
PAGE.data.draging_el = e.target;
e.target.className = 'drag-item draging';
},
// 3. 拖拽移动时,根据鼠标位置与当前项目的位置放置拖拽元素。
itemDragOver: function(e){
let dragEnterItem = e.target;
let id = dragEnterItem.dataset.id;
let parentNode = dragEnterItem.parentNode;
let scrollTop = document.documentElement.scrollTop;
let rect = dragEnterItem.getBoundingClientRect();
let rectTop = rect.top; // 当前项目距离浏览器顶部的距离
let rectHeight = rect.height; // 当前项目的高度
let mouseTop = e.pageY - scrollTop; // 鼠标距离浏览器顶部的距离
if(PAGE.data.draging_id && id !== PAGE.data.draging_id){
if( mouseTop - rectTop >= rectHeight/2){
parentNode.insertBefore(PAGE.data.draging_el,dragEnterItem);
}else{
let nextNode = dragEnterItem.nextSibling;
parentNode.insertBefore(PAGE.data.draging_el,nextNode);
}
}
},
// 4. 拖拽结束时,清除保存的 PAGE.data 拖拽内容,并还原 className
itemDragEnd: function(e){
e.target.className = 'drag-item';
PAGE.data.draging_id = null;
PAGE.data.draging_el = null;
},
}
PAGE.init();
实现拖拽到垃圾桶出删除项目
- 为删除列表委托绑定拖拽进入、拖拽中、拖拽放置事件
- 当列表有项目拖拽进入,阻止默认行为事件
- 当列表有拖拽项目移动时,阻止默认行为事件
- 当列表有拖拽放置时,把当前拖拽元素删除
const PAGE = {
...,
bind: function() {
...,
// 1. 为删除列表委托绑定拖拽进入、拖拽中、拖拽放置事件
this.onEventLister(dragContainer,'dragenter','delete-section',this.sectionEnter);
this.onEventLister(dragContainer,'dragover','delete-section',this.sectionDragOver);
this.onEventLister(dragContainer,'drop','delete-section',this.sectionDrop);
},
...,
// 2. 当列表有项目拖拽进入,把拖拽元素放置在当前列表下
sectionEnter: function(e){
e.preventDefault();
},
// 3. 当列表有拖拽项目移动时,阻止默认行为事件
sectionDragOver: function(e){
e.preventDefault();
},
// 4. 当列表有拖拽放置时,把当前拖拽元素删除
sectionDrop: function(e){
PAGE.data.draging_el.remove();
}
}
PAGE.init();
代码示例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Draggable</title>
<style type="text/css">
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,body{
height: 100%;
}
.drag-container{
height: 100%;
padding: 10px;
display: flex;
}
.doing-section,
.done-section,
.delete-section{
display: flex;
flex-direction: column;
width: 288px;
margin-right: 10px;
background-color: #eee;
border-radius: 3px;
padding: 10px;
}
.sub-title{
padding: 14px 18px;
font-size: 15px;
font-weight: 700;
}
.drag-list{
flex: auto;
}
.drag-item{
background: #fff;
border-radius: 8px;
margin-bottom: 10px;
padding: 12px 10px 12px 20px;
line-height: 20px;
overflow: hidden;
}
.drag-item:hover{
transition: all .2s ease;
border-left: 10px solid #ddd;
cursor: pointer;
}
.drag-item.draging{
background: #f5f5f5;
opacity: .5;
}
</style>
</head>
<body>
<div class="drag-container" id="drag-container">
<div class="doing-section">
<p class="sub-title">进行中</p>
<div class="drag-list">
<div class="drag-item" draggable="true" data-id="1">1</div>
<div class="drag-item" draggable="true" data-id="2">2</div>
<div class="drag-item" draggable="true" data-id="3">3</div>
</div>
</div>
<div class="done-section">
<p class="sub-title">已完成</p>
<div class="drag-list">
<div class="drag-item" draggable="true" data-id="4">4</div>
<div class="drag-item" draggable="true" data-id="5">5</div>
<div class="drag-item" draggable="true" data-id="6">6</div>
</div>
</div>
<div class="delete-section">
<p class="sub-title">回收站</p>
</div>
</div>
<script type="text/javascript">
const PAGE = {
data: {
draging_id: null,
draging_el: null
},
init: function() {
this.bind();
},
bind: function() {
let dragContainer = document.getElementById('drag-container');
this.onEventLister(dragContainer,'dragstart','drag-item',this.itemDragStart);
this.onEventLister(dragContainer,'dragover','drag-item',this.itemDragOver);
this.onEventLister(dragContainer,'dragend','drag-item',this.itemDragEnd);
this.onEventLister(dragContainer,'dragenter','delete-section',this.sectionEnter);
this.onEventLister(dragContainer,'dragover','delete-section',this.sectionDragOver);
this.onEventLister(dragContainer,'drop','delete-section',this.sectionDrop);
},
onEventLister: function(parentNode,action,childClassName,callback) {
parentNode.addEventListener(action,function(e){
e.target.className.indexOf(childClassName) >= 0 && callback(e);
})
},
itemDragStart: function(e){
let id = e.target.dataset.id;
PAGE.data.draging_id = id;
PAGE.data.draging_el = e.target;
e.target.className = 'drag-item draging';
},
itemDragOver: function(e){
let dragEnterItem = e.target;
let id = dragEnterItem.dataset.id;
let parentNode = dragEnterItem.parentNode;
let scrollTop = document.documentElement.scrollTop;
let rect = dragEnterItem.getBoundingClientRect();
let rectTop = rect.top;
let rectHeight = rect.height;
let mouseTop = e.pageY - scrollTop;
if(PAGE.data.draging_id && id !== PAGE.data.draging_id){
if( mouseTop - rectTop >= rectHeight/2){
parentNode.insertBefore(PAGE.data.draging_el,dragEnterItem);
}else{
let nextNode = dragEnterItem.nextSibling;
parentNode.insertBefore(PAGE.data.draging_el,nextNode);
}
}
},
itemDragEnd: function(e){
e.target.className = 'drag-item';
PAGE.data.draging_id = null;
PAGE.data.draging_el = null;
},
sectionEnter: function(e){
e.preventDefault();
},
sectionDragOver: function(e){
e.preventDefault();
},
sectionDrop: function(e){
PAGE.data.draging_el.remove();
},
}
PAGE.init();
</script>
</body>
</html>