任务流程版

-- 拖拽清单的实战应用

本课程主要讲解 JavaScript 拖拽事件。在日常开发中,我们会涉及到拖拽排序、拖拉等的交互情景,就是通过拖拽事件来完成。课程概括了元素拖拽相关的知识点,通过本课程,完成一个拖拽清单的实战应用应用,大家将对 web 页面中拖拽原理和开发有更深入的了解。

主要包含以下功能:

  • 在当前任务列表拖拽排序
  • 状态列表之间可拖拽切换
  • 拖拽到垃圾桶出删除项目

原理

HTML5 的拖放 API 能够支持在网站内部和网站之间拖放项目。同时也提供了一个更简单的供扩展和基于 Mozilla 的应用程序使用的 API。一个典型的drag操作是这样开始的:

  • 用户用鼠标选中一个可拖动的(draggable)元素
  • 移动鼠标到一个可放置的(droppable)元素
  • 然后释放鼠标。

知识点

  1. draggable 属性 想要拖放某个元素,必须设置该元素的 draggable 属性为 true,当该属性为 false 时,将不允许拖放。而 img 元素和 a 元素都默认设置了 draggable 属性为 true,可直接拖放,如果不想拖放这两个元素,把属性设为 false 即可。
<div class="item" draggable="true">可以拖拽元素</div>
  1. 放置区域 当拖动一个项目到HTML元素中时,浏览器默认不会有任何响应。想要让一个元素变成可释放区域,该元素必须设置 dragover 和 drop 事件处理程序属性。dragover 处理程序需调用 preventDefault() 来阻止对这个事件的其它处理过程(如触点事件或指针事件)。

  2. 相关事件

  • 源对象:
    • dragstart:源对象开始拖放。
    • drag:源对象拖放过程中。
    • dragend:源对象拖放结束。
  • 过程对象:
    • dragenter:源对象开始进入过程对象范围内。
    • dragover:源对象在过程对象范围内移动。
    • dragleave:源对象离开过程对象的范围。
  • 目标对象:
    • drop:源对象被拖放到目标对象内。

实现步骤

  1. HTML & CSS
  2. 构建单例对象PAGE
  3. 实现在当前任务列表拖拽排序
  4. 实现拖拽到垃圾桶出删除项目

HTML & CSS

  1. 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>
  1. 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

  1. 定义 PAGE
  2. 定义 PAGE.data 方法
  3. 定义 draging_id、draging_el、draging_pr 拖拽元素 id 、拖拽元素、以及当前的拖拽元素的父级元素
  4. 定义 PAGE.init、PAGE.bind ,并 在PAGE.init 中调用 PAGE.bind
  5. 定义 PAGE.onEventLister 委托绑定方法
  6. 调用 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();

实现在当前任务列表拖拽排序

  1. 为项目委托绑定拖拽开始、拖拽进入、拖拽结束事件
  2. 拖拽开始时,记录当前拖拽元素的 id、元素、父级元素,并修改 className
  3. 拖拽移动时,拖拽移动时,根据鼠标位置与当前项目的位置放置拖拽元素
  4. 拖拽结束时,清除保存的 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();

实现拖拽到垃圾桶出删除项目

  1. 为删除列表委托绑定拖拽进入、拖拽中、拖拽放置事件
  2. 当列表有项目拖拽进入,阻止默认行为事件
  3. 当列表有拖拽项目移动时,阻止默认行为事件
  4. 当列表有拖拽放置时,把当前拖拽元素删除
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>