当前位置:首页 » 《关注互联网》 » 正文

相当流畅的js拖拽排序实现

27 人参与  2024年10月03日 12:00  分类 : 《关注互联网》  评论

点击全文阅读


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(如insertBefore或appendChild)来实现。

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是几个英文单词的缩写。

F:Fist —— 一个元素的起始位置。

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

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

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

直接上带代码:

// Flip.jsconst 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>


点击全文阅读


本文链接:http://zhangshiyu.com/post/167344.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1