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

本文详细介绍了拖拽排序在网页设计中的重要性,如何通过JavaScript和HTML5API实现交互,以及如何结合Flip动画提供平滑的元素移动效果。特别关注了列表排序和图片排序的应用实例。
摘要由CSDN通过智能技术生成

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

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
实现表格的拖拽排序,可以使用以下步骤: 1. 给表格中需要排序的行添加拖拽事件监听器。 2. 在拖拽开始时,记录被拖拽的行的索引。 3. 在拖拽过程中,使用一些视觉效果(比如改变背景颜色)来提示用户当前处于哪一行。 4. 在拖拽结束时,将被拖拽的行移动到目标位置,并更新表格数据。 下面是一个简单的实现示例: HTML: ```html <table id="sortable-table"> <thead> <tr> <th>名称</th> <th>数量</th> <th>价格</th> </tr> </thead> <tbody> <tr><td>苹果</td><td>10</td><td>5</td></tr> <tr><td>香蕉</td><td>15</td><td>3</td></tr> <tr><td>橙子</td><td>8</td><td>4</td></tr> <tr><td>梨子</td><td>12</td><td>6</td></tr> </tbody> </table> ``` JavaScript: ```javascript // 获取表格元素 const table = document.getElementById('sortable-table'); // 获取所有可拖拽的行 const rows = table.tBodies[0].rows; // 记录被拖拽的行的索引 let draggingIndex = -1; // 给所有可拖拽的行添加事件监听器 for (let i = 0; i < rows.length; i++) { const row = rows[i]; // 记录当前行的索引 row.draggingIndex = i; // 添加拖拽事件监听器 row.addEventListener('dragstart', e => { // 记录被拖拽的行的索引 draggingIndex = e.target.draggingIndex; // 设置拖拽效果 e.dataTransfer.effectAllowed = 'move'; // 设置拖拽数据 e.dataTransfer.setData('text/html', row.innerHTML); }); // 添加拖拽过程中的事件监听器 row.addEventListener('dragover', e => { // 阻止默认行为,使得drop事件能够触发 e.preventDefault(); // 设置拖拽效果 e.dataTransfer.dropEffect = 'move'; // 获取当前鼠标所在的位置 const y = e.clientY - table.offsetTop; // 遍历所有行,找到当前鼠标所在的位置应该插入的位置 for (let j = 0; j < rows.length; j++) { const row = rows[j]; if (y > row.offsetTop && y < row.offsetTop + row.offsetHeight) { // 如果拖拽的行和目标行不是同一行,则交换位置 if (draggingIndex !== j) { table.tBodies[0].insertBefore(rows[draggingIndex], j > draggingIndex ? rows[j] : rows[j + 1]); break; } } } }); // 添加拖拽结束的事件监听器 row.addEventListener('dragend', e => { // 重置被拖拽的行的索引 draggingIndex = -1; }); } ``` 在这个示例中,我们使用了HTML5的拖拽API来实现表格的拖拽排序。在拖拽过程中,我们通过改变被拖拽的行和目标行的位置来实现排序。需要注意的是,这个示例只是一个简单的实现,如果需要支持更复杂的排序方式,比如拖拽多个行或者跨页排序,还需要进行更多的功能扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晨曦_子画

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值