先上效果:
实现思路和流程:
- 基础页面布局 给每个拖动元素加上 draggable="true"
- ondragstart(开始拖动某个元素时)做出 对应的处理 获得操作的具体元素 给目标元素添加对应的样式 显示透明 增加虚线描边
- ondragover 被拖动的元素hover到目标元素上时触发 阻止默认事件-默认不让元素拖动到自身
- ondragenter 拖动进行当中 对比 当前拖动的元素和 正在覆盖元素的索引 来判断操作 是上升还是下降
- ondragend 在结束时 样式由内部透明 虚线转为 原来的样子
- 配置flip动画
基础页面布局:
<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> <style> .list{ width: 750px; margin: 40px auto; } .list_item{ width: 100%; border-radius: 8px; height: 52px; margin-bottom: 12px; background: rgba(40,142,145,0.9); color: white; line-height: 52px; padding-left: 16px; font-size: 18px; box-sizing: border-box; cursor: move; /*user-select: none;*/ } .moving{ background: transparent; color: transparent; border: 1px dashed #ccc; } </style>
复制
实现拖动步骤
获取到总的外容器 便于下面事件委托 const list = document.querySelector('.list') 获取到所有可拖动的元素 用于记录起始位置 const item = document.querySelectorAll('.list_item') let sourceNode; 判断当前拖动的是哪个元素 开始拖动的事件 list.ondragstart = e =>{ sourceNode = e.target record(item) 传入item 记录起始位置 setTimeout(()=>{ e.target.classList.add('moving') },0) e.dataTransfer.effectAllowed = 'move' } list.ondragover = e => { e.preventDefault() } 拖动进行中的事件 list.ondragenter = e =>{ e.preventDefault() 托回到原来的位置了就什么也不做 if(e.target === list || e.target === sourceNode){ return false } const children = Array.from(list.children) const sourceIndex = children.indexOf(sourceNode) 当前劫持元素的索引值 const targetIndex = children.indexOf(e.target) 覆盖到谁上面的索引值 if(sourceIndex < targetIndex){ 父节点.insertBefore(要插入的节点,在谁前面) 从下向上拖动 list.insertBefore(sourceNode,e.target.nextElementSibling) }else { list.insertBefore(sourceNode,e.target) } last([e.target,sourceNode]) 传入改变位置的两个元素 比较差异 执行filp动画 } 拖动结束的时候取消虚线 list.ondragend = e =>{ e.target.classList.remove('moving') }复制filp动画的函数
// 记录初始位置 function record(eleAll) { for( let i = 0;i < eleAll.length; i++ ) { const { top,left } = eleAll[i].getBoundingClientRect() eleAll[i]._top_ = top eleAll[i]._left_ = left } } /* getBoundingClientRect()用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗(可视范围不包含卷去的部分)的位置。*/ /** * requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点: 1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。 2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。 取消:cancelAnimationFrame(Id) * **/ // 记录最后的位置 并且执行动画 function last(eleAll) { for( let i = 0;i < eleAll.length; i++ ) { const dom = eleAll[i] const { top,left } = dom.getBoundingClientRect() // 新增dom时,逻辑应为 原有dom后移动,新增dom不动,故记录了位置的才添加动画 确定上一步有记录起始位置再进行下一步 if(dom._left_) { // 恢复至开始位置 dom.style.transform = `translate3d(${ dom._left_ - left }px, ${ dom._top_ - top }px,0px)` // play 过程,移除开始位置的设置,添加过渡 let rafId = requestAnimationFrame(function() { //启用tansition,并移除翻转的改变 可以内置样式也可以用 外部类 //dom.classList.add('active') dom.style.transition = 'transform 300ms ease-out' dom.style.transform = 'none' }) dom.addEventListener('transitionend', () => { dom.style.transition = 'none' //dom.classList.remove('active') cancelAnimationFrame(rafId) }) } } }复制
flip 动画思路
f - first 记录动画开始前的位置、大小等信息 ( translateY(0px) )
l - last 记录动画结束时的位置、大小等信息 ( translateY(100px) )
i - invert 对动画前后数据信息的计算(translateY --> 100px,同时利用translate等操作,将dom恢复到 first位置)
p - play 开始动画,并移除 i 步骤恢复至 first 的操作,启用tansition,动画就开始了
整个过程其实就是,先记录好动画前后的dom位置等数据信息
然后,利用css将dom恢复至初始位置
最后,移除上一步恢复的状态(此时dom会自动回到last位置,只不过没有过渡效果,生硬的闪现),添加过渡效果,完成动画
复制
完整代码:
const list = document.querySelector('.list') const item = document.querySelectorAll('.list_item') let sourceNode; list.ondragstart = e =>{ sourceNode = e.target record(item) setTimeout(()=>{ e.target.classList.add('moving') },0) e.dataTransfer.effectAllowed = 'move' } list.ondragover = e => { e.preventDefault() } list.ondragenter = e =>{ e.preventDefault() if(e.target === list || e.target === sourceNode){ return false } const children = Array.from(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) } last([e.target,sourceNode]) } list.ondragend = e =>{ e.target.classList.remove('moving') } function record(eleAll) { for( let i = 0;i < eleAll.length; i++ ) { const { top,left } = eleAll[i].getBoundingClientRect() eleAll[i]._top_ = top eleAll[i]._left_ = left } } function last(eleAll) { for( let i = 0;i < eleAll.length; i++ ) { const dom = eleAll[i] const { top,left } = dom.getBoundingClientRect() if(dom._left_) { dom.style.transform = `translate3d(${ dom._left_ - left }px, ${ dom._top_ - top }px,0px)` let rafId = requestAnimationFrame(function() { dom.style.transition = 'transform 300ms ease-out' dom.style.transform = 'none' }) dom.addEventListener('transitionend', () => { dom.style.transition = 'none' cancelAnimationFrame(rafId) }) } } }
复制