vue列表拖拽排序功能实现
1.实现目标:目标是输入一个数组,生成一个列表;通过拖拽排序,拖拽结束后输出一个经过排序的数组。
2.实现思路:
2.1是使用HTML5的drag功能来实现,每次拖拽时直接操作Dom节点排序,拖拽结束后再根据实际的dom节点遍历得出新的数组。
2.2使用mousedown,mouseover等鼠标事件来实现,每次监听事件时,仅改动列表项的样式transform,而不操作实际的dom顺序。拖拽结束时,根据transform计算数组项顺序,得出新数组用vue数据驱动的方式重绘列表,重置所有样式。
总的来说就是可以通过不同的监听事件(drag、mouseover),按不同的顺序操作Dom(1.先操作实际dom,再添加动画,在输出数组;2。不操作实际dom,仅改变transfrom,得出新数组,用新数组生成新列表来更新节点)。
3.实际代码
3.1第一种实现
html部分。(被拖拽的元素需要设置draggable=true,否则不会有效果)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | < div id="app"> < ul @dragstart="onDragStart" @dragover="onDragOver" @dragend="onDragEnd" ref="parentNode"> < li v-for="(item,index) in data" :key="index" class="item" draggable="true" >{{item}}</ li > </ ul > </ div > |
拖拽事件有两个对象(被拖拽对象和目标对象)。dragstart 事件: 当拖拽元素开始被拖拽的时候触发的事件,此事件作用在被拖拽元素上。dragover事件:当拖拽元素穿过目标元素时候触发的事件,此事件作用在目标元素上。
在拖拽事件开始时,将本次拖拽的对象保存到变量中。每当dragover事件,将目标对象保存到变量中,添加判断当目标对象和拖拽对象为不同的列表项时,交换两个dom元素的先后顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | onDragStart(event){ console.log("drag start") this.draging=event.target; }, onDragOver(event){ console.log('drag move') this.target=event.target; if (this.target.nodeName === "LI" && this.target !== this.draging) { if(this._index(this.draging)< this._index (this.target)){ this.target.parentNode.insertBefore(this.draging,this.target.nextSibling); }else{ this.target.parentNode.insertBefore(this.draging,this.target); } } }, onDragEnd(event){ console.log('drag end') let currentNodes=Array.from(this.$refs.parentNode.childNodes); let data=currentNodes.map((i,index)=>{ let item=this.data.find(c=>c==i.innerText); return item }); console.log(data) }, _index(el){ let domData=Array.from(this.$refs.parentNode.childNodes); return domData.findIndex(i=>i.innerText==el.innerText); } |
现在基本效果有了,然后是添加动画。添加动画的方式是通过transform实现。
因为每次拖拽排序触发时都会改变dom结构,为了实现移动的效果,可以在每次排序时先将dom节点恢复通过transform到原来的位置,使得表现上还是排序前的状态。然后添加transition,同时置空transform实现移动效果。(这里需要重绘才能触发效果,否则两次transform会直接抵消掉,可以使用setTimeout或者ele.offsetWidth来触发重绘),transform的偏移量可以通过改变节点顺序前后的距顶高度来获得。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | <! DOCTYPE html> < html > < head > < meta charset="utf-8"> < meta name="viewport" content="width=device-width,initial-scale=1.0"> < script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></ script > < style > ul{ list-style:none; padding-bottom:20px; } .item{ cursor: pointer; height:24px; line-height:24px; background-color:#9c9c9c; border:1px solid #d9d9d9; border-radius:4px; color:#fff; padding:10px; } </ style > </ head > < body > < div id="app"> < ul @dragstart="onDragStart" @dragover="onDragOver" @dragend="onDragEnd" ref="parentNode"> < li v-for="(item,index) in data" :key="index" class="item" draggable="true" >{{item}}</ li > </ ul > </ div > </ body > < script > var app = new Vue({ el: '#app', data: { data:[1,2,3,4,5,6], draging:null,//被拖拽的对象 target:null,//目标对象 }, mounted () { //为了防止火狐浏览器拖拽的时候以新标签打开,此代码真实有效 document.body.ondrop = function (event) { event.preventDefault(); event.stopPropagation(); } }, methods:{ onDragStart(event){ console.log("drag start") this.draging=event.target; }, onDragOver(event){ console.log('drag move') this.target=event.target; let targetTop=event.target.getBoundingClientRect().top; let dragingTop=this.draging.getBoundingClientRect().top; if (this.target.nodeName === "LI"&&this.target !== this.draging) { if (this.target) { if (this.target.animated) { return; } } if(this._index(this.draging)< this._index (this.target)){ this.target.parentNode.insertBefore(this.draging,this.target.nextSibling); }else{ this.target.parentNode.insertBefore(this.draging, this.target); } this._anim(targetTop,this.target); this._anim(dragingTop,this.draging); } }, _anim(startPos,dom){ let offset=startPos-dom.getBoundingClientRect().top; dom.style.transition="none"; dom.style.transform=`translateY(${offset}px)`; //触发重绘 dom.offsetWidth; //触发重绘 // setTimeout(()=>{ // dom.style.transition="transform .3s"; // dom.style.transform=``; // },0) clearTimeout(dom.animated); dom.animated=setTimeout(()=>{ dom.style.transition=""; dom.style.transform=``; dom.animated=false; },300) }, onDragEnd(event){ console.log('drag end') let currentNodes=Array.from(this.$refs.parentNode.childNodes); let data=currentNodes.map((i,index)=>{ let item=this.data.find(c=>c==i.innerText); return item }); console.log(data) }, _index(el){ let domData=Array.from(this.$refs.parentNode.childNodes); return domData.findIndex(i=>i.innerText==el.innerText); } } }) </ script > </ html > |
3.2.第二种实现
mousedown的时候记录下拖拽项和拖拽项初始位置,mouseover的时候将拖拽项和目标项交换位置,添加transform,mouseup的时候遍历出新数组来更新视图。这种方式就是动画不好加,个人瞎琢磨的,应该是思路错误了,放着看看吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | <! DOCTYPE html> < html > < head > < meta charset="utf-8"> < meta name="viewport" content="width=device-width,initial-scale=1.0"> < script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></ script > < style > ul{ list-style:none; padding-bottom:20px; } .item{ cursor: pointer; height:24px; line-height:24px; background-color:#9c9c9c; border:1px solid #d9d9d9; border-radius:4px; color:#fff; padding:10px; user-select: none; } </ style > </ head > < body > < div id="app"> < ul ref="parentNode" @mouseover="onMouseOver" @mouseup="onMouseUp"> < li ref="li" v-for="(item,index) in data" :key="index" class="item" @mouseDown="(event)=>{onMouseDown(event,index)}" >{{item}}</ li > </ ul > </ div > </ body > < script > var app = new Vue({ el: '#app', data: { data:[1,2,3,4,5,6], isDonw:false, draging:null, dragStartPos:0 }, mounted () { //为了防止火狐浏览器拖拽的时候以新标签打开,此代码真实有效 document.body.ondrop = function (event) { event.preventDefault(); event.stopPropagation(); } document.onmouseup=()=>{ if(this.isDonw) this.onMouseUp() }; }, computed:{ nodes(){ return Array.from(this.$refs.parentNode.children) }, itemHeight(){ return this.nodes[0].offsetHeight; } }, methods:{ onMouseDown(event,index){ this.isDonw=true; this.draging=this.$refs['li'][index]; this.dragStartPos=this.draging.getBoundingClientRect().top; }, onMouseOver(event){ if(this.isDonw){ let target=event.target; let drag=this.draging; let Index=this._index(target); if(target.nodeName!='UL' && target!=drag){ let targetTop=target.getBoundingClientRect().top; let dragTop=drag.getBoundingClientRect().top; let targetOffset=targetTop-dragTop; let dragOffset=targetTop-this.dragStartPos; //样式变化 let targetStyle= target.style.transform; let lastTransform=0; if(targetStyle){ lastTransform=this.getTransform(targetStyle); } drag.style.transform=`translateY(${dragOffset}px)`; target.style.transform=`translateY(${lastTransform-targetOffset}px)`; } } }, onMouseUp(){ this.isDonw=false; this.draging=null; this.dragStartPos=0; let res=[] for(let i=0;i< this.nodes.length ;i++){ let item=this.nodes[i]; let transform=this.getTransform(item.style.transform); if(transform){ res[i+transform/this.itemHeight]=this.data[i]; }else{ res[i]=this.data[i]; } item.style.transform=''; item.style.transition=''; } this.data=[...res]; console.log(res) }, getTransform(style){ if(style){ let firstIndex=style.indexOf('(')+1; let lastIndex=style.indexOf(')')-2; return parseInt(style.substring(firstIndex,lastIndex)) } }, _index(el){ let domData=Array.from(this.$refs.parentNode.childNodes); return domData.findIndex(i=>i.innerText==el.innerText); } } }) </ script > </ html > |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步