LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

【JavaScript】手摸手教你实现一个自己的拖拽排序

admin
2024年4月12日 23:19 本文热度 675
01
写在前面

拖拽排序是一种在网页设计和应用程序中常见的交互方式,允许用户通过鼠标或触摸操作来重新排列页面或界面上的元素。这种交互方式对于提升用户体验和操作效率具有重要意义。


在拖拽排序中,用户可以用鼠标或手指按住某个元素,然后将其拖动到新

的位置,从而实现对元素的重新排列。这种操作直观且灵活,使得用户可以根据自己的需求随时调整页面或界面的布局,提升了个性化体验。同时,拖拽排序也增加了用户的参与度和粘性,用户可以通过自由选择和排序感兴趣的内容,提升留存率和活跃度。


从技术实现的角度来看,拖拽排序主要依赖于前端技术的支持。例如,基于JavaScript的实现方法主要是通过监听鼠标或触摸事件来实现。在拖拽开始时,需要记录拖拽元素的位置,然后在拖拽过程中更新元素的位置,最后在拖拽结束时判断元素与其他元素的位置关系并进行排序。


在拖拽排序的应用场景中,列表排序和图片排序是两个典型的例子。在列表排序中,用户可以通过拖动列表项来改变它们的顺序,这在任务管理应用、待办事项列表等场景中非常常见。在图片排序中,用户可以通过拖动图片来改变它们的顺序,这在图片库或相册应用中较为常见。


02
实现


在HTML中,我们给需要拖动的元素加上draggable="true"就可以实现拖拽效果了。在CSS中,我们设置了列表和拖拽项的样式。

<div class="list">
    <div draggable="true" class="list-item">1</div>
    <div draggable="true" class="list-item">2</div>
    <div draggable="true" class="list-item">3</div>
    <div draggable="true" class="list-item">4</div>
    <div draggable="true" class="list-item">5</div>
    <div draggable="true" class="list-item">6</div>
</div>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    display: flex;
    justify-content: center;
}
.list {
    width: 600px;
    margin-top: 100px;
}
.list-item {
    margin: 6px 0;
    padding: 0 20px;
    line-height: 40px;
    height: 40px;
    background: #409eff;
    color: #fff;
    text-align: center;
    cursor: move;
    user-select: none;
    border-radius: 5px;
}

效果如下:

元素是可以拖拽了,但是拖拽时元素本身的样式要改变,我们只需要给元素加上一个类样式就可以了。那么,什么时候添加这个类呢?当然是开始拖动的时候,我们使用了HTML5的拖放API ondragstart,它是在用户开始拖动元素时触发


2.1 拖拽开始

我们找到拖拽项的父元素,用事件委托的方式找到父元素,也就.list并给它注册一个ondragstart事件,当拖拽开始时,可以使用event.target来获取被拖拽的元素,给它的类型样式添加一个moving。

.list-item.moving {
    background: transparent;
    color: transparent;
    border: 1px dashed #ccc;
}
const list = document.querySelector('.list');

list.ondragstart = (e) => {
    setTimeout(() => {
        e.target.classList.add('moving')
    }, 0)
}

为什么要加setTimeout呢?因为跟随鼠标的样式取决于拖拽开始时元素本身的样式,拖转开始时把元素的样式改变了,那就意味着跟随鼠标的样式也改变了,我们可以加一个setTimeout变成异步,在拖拽开始时还是保持原来的样式,然后过一点点时间在变成添加moving的样式。


2.2 拖拽过程

(1)当被拖拽的元素移动到另一个列表项上方时,会不断触发dragover事件。

(2)默认情况下,浏览器不允许放置(drop)操作,因此需要阻止这个事件的默认行为。这可以通过调用event.preventDefault()方法来实现。

ondragover: 当某被拖动的对象在另一对象容器范围内拖动时触发此事件。

list.ondragover = (e) => {
    e.preventDefault();
}

(3)当用户释放鼠标按钮,且被拖拽的元素位于一个有效的放置目标上方时,drop事件被触发。

(4)在drop事件处理程序中,首先需要获取拖拽源元素接着取放置目标元素,这通常是触发drop事件的元素。

(5)然后,需要更新DOM来反映新的排序。这通常涉及改变元素的位置,可以通过直接操作DOM(如insertBeforeappendChild)来实现。

ondragenter:当被鼠标拖动的对象进入其容器范围内时触发此事件。

const list = document.querySelector('.list');
// 记录被拖拽的元素
let sourceNode;

list.ondragstart = (e) => {
    setTimeout(() => {
        e.target.classList.add('moving')
    }, 0)
    // 记录被拖拽的元素
    sourceNode = e.target;
}

list.ondragover = (e) => {
    e.preventDefault();
}

list.ondragenter = e => {
    e.preventDefault();
    // 判断拖拽元素进入的元素等于父元素list或等于拖拽元素本身,
    // 不做受任何处理,直接结束
    if(e.target === list || e.target === sourceNode) {
        return;
    }
    // 判断元素拖拽进入的位置是在目标的上面还是下面,
    // 比如拖动3进入到4时,4要移动到上面,
    // 当拖动3进入到2时,2要移动到下面,
    // 通过元素所处的下表既可判断。

    // 首先,拿到元素list所有的子元素
    const children = [...list.children];
    // 接着,拿到要拖拽元素在整个子元素里面的下标
    const sourceIndex = children.indexOf(sourceNode);
    // 然后,拿到要进入目标元素在整个子元素里面的下标
    const targetIndex = children.indexOf(e.target);
    if(sourceIndex < targetIndex) {
        // 进入目标元素大于拖拽元素的下标,
        // 此时要插入目标元素的下方位置,
        // 也就是目标元素下一个元素的前面
        list.insertBefore(sourceNode, e.target.nextElementSibling);
    } else {
        // 进入目标元素小于拖拽元素的下标,
        // 此时要插入目标元素的上方位置,
        // 也就是目标元素前面的位置
        list.insertBefore(sourceNode, e.target);
    }
}

2.3 拖拽结束

ondragend:用户完成元素拖动后触发。

list.ondragend = () => {
  sourceNode.classList.remove('moving');
}

拖拽结束时,只需要把moving的样式移除即可。


03
Flip动画


为了使元素位置改变时不那么生硬,可能需要提供一些额外的反馈,可以通过动画来平滑地展示元素位置的改变。那么我们来了解一种动画——Flip动画。什么是Flip动画呢?

Flip技术可以让我们的动画更加流畅,同时也能降低复杂动画的开发难度。其实,Flip是几个英文单词的缩写。

FFist —— 一个元素的起始位置。

L:Last —— 另一个元素的终止位置,注意另一个这个词,后面会有具体代码的体现。

I:Invert —— 计算"F"与"L"的差异,包括位置,大小等,并将差异用transform属性,添加到终止元素上,让它回到起始位置,也是此项技术的核心。

P:Play —— 添加transtion 过渡效果,清除Invert阶段添加进来transform,播放动画。


直接上带代码:

// Flip.js
const Flip = (function () {
 class FlipDom {
  constructor(dom, duration = 0.5) {
   this.dom = dom;
   this.transition =
    typeof duration === 'number' ? `${duration}s` : duration;
   this.firstPosition = {
    x: null,
    y: null,
   };
   this.isPlaying = false;
   this.transitionEndHandler = () => {
    this.isPlaying = false;
    this.recordFirst();
   }
  }

  getDomPosition() {
   const rect = this.dom.getBoundingClientRect();
   return {
    x: rect.left,
    y: rect.top,
   }
  }

  recordFirst(firstPosition) {
   if (!firstPosition) {
    firstPosition = this.getDomPosition()
   }
   this.firstPosition.x = firstPosition.x;
   this.firstPosition.y = firstPosition.y;
  }

  * play() {
   if (!this.isPlaying) {
    this.dom.style.transition = 'none';
    const lastPosition = this.getDomPosition();
    const dis = {
     x: lastPosition.x - this.firstPosition.x,
     y: lastPosition.y - this.firstPosition.y,
    }
    if (!dis.x && !dis.y) {
     return;
    }
    this.dom.style.transform = `translate(${-dis.x}px, ${-dis.y}px)`;
    yield 'moveToFirst';
    this.isPlaying = true;
   }
   this.dom.style.transition = this.transition;
   this.dom.style.transform = 'none';
   this.dom.removeEventListener('transitionend', this.transitionEndHandler);
   this.dom.addEventListener('transitionend', this.transitionEndHandler);
  }
 }

 class Flip {
  constructor(doms, duration = 0.5) {
   this.flipDoms = [...doms].map((it) => new FlipDom(it, duration));
   this.flipDoms = new Set(this.flipDoms);
   this.duration = duration;
   this.flipDoms.forEach((it) => it.recordFirst());
  }

  addDom(dom, firstPosition) {
   const flipDom = new FlipDom(dom, this.duration);
   this.flipDoms.add(flipDom)
   flipDom.recordFirst(firstPosition)
  }

  play() {
   let gs = [...this.flipDoms].map((it) => {
     const generator = it.play();
     return {
      generator,
      iteratorResult: generator.next()
     }
    })
    .filter((g) => !g.iteratorResult.done);

   while (gs.length > 0) {
    document.body.clientWidth;
    gs = gs.map((g) => {
      g.iteratorResult = g.generator.next();
      return g;
     })
     .filter((g) => !g.iteratorResult.done);
   }
  }
 }
 return Flip;
})();


完整代码如下:

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title></title>
  <style>
   * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
   }
   body {
    display: flex;
    justify-content: center;
   }
   .list {
    width: 600px;
    margin-top: 100px;
   }
   .list-item {
    margin: 6px 0;
    padding: 0 20px;
    line-height: 40px;
    height: 40px;
    background: #409eff;
    color: #fff;
    text-align: center;
    cursor: move;
    user-select: none;
    border-radius: 5px;
   }
   .list-item.moving {
    background: transparent;
    color: transparent;
    border: 1px dashed #ccc;
   }
  </style>
 </head>
 <body>
  <div class="list">
   <div draggable="true" class="list-item">1</div>
   <div draggable="true" class="list-item">2</div>
   <div draggable="true" class="list-item">3</div>
   <div draggable="true" class="list-item">4</div>
   <div draggable="true" class="list-item">5</div>
   <div draggable="true" class="list-item">6</div>
  </div>
 </body>
 <script src="./flip.js"></script>
 <script>
  const list = document.querySelector('.list');
  // 记录被拖拽的元素
  let sourceNode;
  let flip;
  
  list.ondragstart = (e) => {
   setTimeout(() => {
    e.target.classList.add('moving')
   }, 0)
   sourceNode = e.target;
   flip = new Flip(list.children, 0.5);
  }
  
  list.ondragover = (e) => {
   e.preventDefault();
  }
  
  list.ondragenter = e => {
   e.preventDefault();
   // 判断拖拽元素进入的元素等于父元素list或等于拖拽元素本身,
   // 不做受任何处理,直接结束
   if(e.target === list || e.target === sourceNode) {
    return;
   }
   // 判断元素拖拽进入的位置是在目标的上面还是下面,
   // 比如拖动3进入到4时,4要移动到上面,
   // 当拖动3进入到2时,2要移动到下面,
   // 通过元素所处的下表既可判断。
   
   // 首先,拿到元素list所有的子元素
   const children = [...list.children];
   // 接着,拿到要拖拽元素在整个子元素里面的下标
   const sourceIndex = children.indexOf(sourceNode);
   // 然后,拿到要进入目标元素在整个子元素里面的下标
   const targetIndex = children.indexOf(e.target);
   if(sourceIndex < targetIndex) {
    // 进入目标元素大于拖拽元素的下标,
    // 此时要插入目标元素的下方位置,
    // 也就是目标元素下一个元素的前面
    list.insertBefore(sourceNode, e.target.nextElementSibling);
   } else {
    // 进入目标元素小于拖拽元素的下标,
    // 此时要插入目标元素的上方位置,
    // 也就是目标元素前面的位置
    list.insertBefore(sourceNode, e.target);
   }
   // 调用flip动画play方法
   flip.play();
  }
  
  list.ondragend = () => {
   sourceNode.classList.remove('moving');
  }
 </script>
</html>


该文章在 2024/4/12 23:19:02 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved