# 列表拖拽的Vue组件
效果如下
# 1、组件本身
ListDrag.vue
<script>
const itemHeight = 30;
export default {
name: 'ListDrag',
// mixins: [],
// components: {},
props: {
// 自定义内容类型
renderContent: {
type: Function,
default: (h) => {
return h('span', '');
}
},
value: {
type: Array,
default: () => []
},
listKey: String
},
// emits: ['input'],
data() {
return {
list: []
};
},
// computed: {},
// filters: {},
watch: {
value: {
handler: function (newValue = []) {
this.list = [...newValue];
},
deep: true
}
/* list: {
handler: function (newList) {
this.$emit(newList);
},
deep: true
} */
},
created() {
this.list = [...this.value];
this.dragTarget = null;
this.allItemElList = [];
this.toIndex = -1;
},
// mounted() {},
// beforeRouteEnter(to, from, next) { next(vm => {}) },
// beforeRouteUpdate(to, from, next) {},
// beforeRouteLeave(to, from, next) {},
// beforeDestroy() {},
// destroyed() {},
methods: {
onDragstartHandler(e) {
// e.preventDefault();
console.debug('onDragstartHandler', e);
const target = this.findDraggableListItem(e.target);
if (target) {
this.dragTarget = target;
}
this.allItemElList = this.$el.querySelectorAll('.drag-list-item');
},
onDragoverHandler(e) {
e.preventDefault();
const liList = this.allItemElList;
for (let i = 0, len = liList.length; i < len; i++) {
const liEl = liList[i];
const { top, height } = liEl.getBoundingClientRect();
const clientY = e.clientY;
if (i === 0) {
if (clientY <= top + height / 2) {
// 说明需要插入到最上面
this.toIndex = 0;
this.setActiveElStyle(liEl);
break;
}
}
if (i === len - 1) {
if (clientY > top + height / 2) {
// 说明需要插入到底部
// container.appendChild(dragTarget);
this.toIndex = this.allItemElList.length;
this.setActiveElStyle(liEl);
// insertBeforeReferenceNode = null;
break;
}
}
const nextLiEl = liList[i + 1];
if (liEl && nextLiEl) {
const { top: nextTop, height: nextHeight } = nextLiEl.getBoundingClientRect();
const middleLine = top + height / 2;
const nextMiddleLine = nextTop + nextHeight / 2;
if (middleLine < clientY && clientY <= nextMiddleLine) {
// 说明在这两个元素中间
this.toIndex = i + 1;
this.setActiveElStyle(liEl);
}
}
}
},
onDragendHandler(e) {
e.preventDefault();
console.debug('onDragendHandler', e);
// this.allItemElList = []
if (this.dragTarget) {
const fromIndex = this.dragTarget.dataset.index - 0;
console.debug('fromIndex = ', fromIndex, 'toIndex = ', this.toIndex);
if (fromIndex === this.toIndex) {
// 不用移动
this.toIndex = -1;
this.resetActiveElStyle();
return;
}
if (fromIndex < this.toIndex) {
// 从前往后移 先插后删
const fromData = this.list[fromIndex];
const tempList = [...this.list];
tempList.splice(this.toIndex, 0, fromData);
tempList.splice(fromIndex, 1);
this.list = tempList;
}
if (fromIndex > this.toIndex) {
// 从后往前移 先删后插
const fromData = this.list[fromIndex];
const tempList = [...this.list];
tempList.splice(fromIndex, 1);
tempList.splice(this.toIndex, 0, fromData);
this.list = tempList;
}
this.toIndex = -1;
this.$emit('input', this.list);
this.resetActiveElStyle();
}
},
onDragenterHandler(e) {
e.preventDefault();
e.stopPropagation();
},
onDragleaveHandler(e) {
e.preventDefault();
e.stopPropagation();
},
onDropHandler(e) {
// e.preventDefault();
console.debug('onDropHandler', e);
},
setActiveElStyle(el) {
this.resetActiveElStyle();
if (this.toIndex === 0) {
el.style.marginTop = itemHeight + 'px';
} else {
el.style.marginBottom = itemHeight + 'px';
}
},
resetActiveElStyle() {
const liList = this.allItemElList;
for (let i = 0, len = liList.length; i < len; i++) {
const el = liList[i];
el.style.marginBottom = '0';
el.style.marginTop = '0';
}
},
swapList(list, index1, index2) {
const temp = list[index1];
list[index1] = list[index2];
list[index2] = temp;
return list;
},
findDraggableListItem(
el,
predicate = (el) => {
try {
return el.getAttribute('draggable') === 'true';
} catch (error) {
return false;
}
}
) {
if (!el) return null;
let target = el;
while (target) {
if (predicate(target)) {
break;
} else {
target = target.parentNode;
}
}
return target;
},
// 向外提供接口 用于获取最新的列表顺序和内容
getFinalList() {
return this.list;
},
handleMoveUp(index) {
return (event) => {
event.stopPropagation();
console.log(index, event.target);
if (index === 0) {
return;
}
this.list = this.swapList([...this.list], index, index - 1);
console.log(this.list, '==');
this.$emit('input', this.list);
};
},
handleMoveDown(index) {
return (event) => {
console.log(index, event.target);
event.stopPropagation();
if (index === this.list.length - 1) {
return;
}
this.list = this.swapList([...this.list], index, index + 1);
console.log(this.list, '==');
this.$emit('input', this.list);
};
},
handleDelete(index) {
return (event) => {
event.stopPropagation();
const deleteData = this.list.splice(index, 1)[0];
this.$emit('input', this.list);
this.$emit('delete', deleteData);
};
}
},
render(h) {
return h(
'div',
{
class: {
'list-drag__wrapper': true
},
on: {
dragstart: this.onDragstartHandler,
dragend: this.onDragendHandler,
dragover: this.onDragoverHandler,
dragenter: this.onDragenterHandler,
dragleave: this.onDragleaveHandler,
drop: this.onDropHandler
}
},
[
h(
'transition-group',
{
props: {
name: 'xd-flip-list',
tag: 'ul'
}
},
this.list.map((item, index) => {
const key = item[this.listKey];
return h(
'li',
{
key: key,
class: {
'drag-list-item': true
},
attrs: {
draggable: true,
'data-index': index,
'data-key': key,
'data-identify': 'drag-item'
}
},
[
h(
'div',
{
class: { 'prefix-icon': true }
},
'⁝⁝'
),
h(
'div',
{
class: { item__content: true }
},
[this.renderContent(h, item, index)]
),
h(
'span',
{
class: { 'move-up__btn': true, 'move-btn--disabled': index === 0 },
props: { name: 'shangyi' },
on: {
click: this.handleMoveUp(index)
}
},
'上'
),
h(
'span',
{
class: { 'move-down__btn': true, 'move-btn--disabled': index === this.list.length - 1 },
props: { name: 'xiayi' },
on: {
click: this.handleMoveDown(index)
}
},
'下'
),
h(
'span',
{
class: { delete__btn: true },
props: { name: 'close' },
on: {
click: this.handleDelete(index)
}
},
'删'
)
]
);
})
)
]
);
}
};
</script>
<style lang="stylus" scoped>
$item-height = 30px;
.list-drag__wrapper {
padding: 1px 0;
.drag-list-item {
background-color: #eee;
height: $item-height;
display: flex;
box-sizing: border-box;
align-items: center;
padding: 0 4px;
transition: margin 0.05s linear, transform 0.2s;
}
.item__content {
flex: 1;
overflow: hidden;
height: 100%;
display: flex;
align-items: center;
}
.prefix-icon {
width: 24px;
font-size: 16px !important;
text-align: center;
cursor: move;
}
.move-up__btn, .move-down__btn, .delete__btn {
font-size: 12px !important;
margin-left: 6px;
}
.move-up__btn {
cursor: n-resize;
}
.move-down__btn {
cursor: s-resize;
}
.move-btn--disabled {
color: #c3cbd6;
cursor: not-allowed;
}
}
</style>
# 2、组件用法
通过 renderContent
自定义需要渲染的内容
WARNING
这里其实就是模板语法的不足之后,有些需要通过slot自定义传入的内容,在这里就不适用, 尤其还需要v-for
来循环生成动态内容时。 还在还有render
可用。
listKey 是列表对象的唯一key值,用于v-for循环。
也支持v-model,前提是你的列表不需要额外特殊逻辑。
如果需要动态增加内容, 比如输入框, 下拉框等还需要修改列表的值时, input 事件回调就需要自己定义。如下面的逻辑
<template>
<div style="max-width: 500px;">
<ListDrag :value="list"
:renderContent="renderContent"
:listKey="__id__"
@input="onInput"
@delete="onDelete"></ListDrag>
<button @click="handleAdd">add</button>
</div>
</template>
<script>
// import { deepClone2 } from './utils/utils.js';
import ListDrag from './ListDrag.vue';
const __id__ = 'id_drag';
export default {
name: 'Index',
// mixins: [],
components: {
ListDrag
},
// props: {},
data() {
return {
list: []
};
},
// computed: {},
// filters: {},
// watch:{},
created() {
this.__id__ = __id__;
this.__index__ = 0;
this.listMap = new Map();
this.getList();
},
// mounted() {},
// beforeRouteEnter(to, from, next) { next(vm => {}) },
// beforeRouteUpdate(to, from, next) {},
// beforeRouteLeave(to, from, next) {},
// beforeDestroy() {},
// destroyed() {},
methods: {
getList() {
const tempList = [];
for (let i = 0, len = 8; i < len; i++) {
const id = this.__index__.toString();
const dataObj = { data: this.__index__, [__id__]: id };
this.__index__++;
tempList.push(dataObj);
this.listMap.set(id, dataObj);
}
this.list = tempList;
},
renderContent(h, item, index) {
// 可以根据唯一key值 渲染任何自己想要的东西
/* if (item[__id__] == '3') {
const label = this.list[index].name;
return h('div', {}, [
h('input', {
attrs: {
value: label,
'data-name': label
},
on: {
input: (e) => {
console.log(e);
// this.list[index].name = e.target.value;
this.$set(this.list[index], 'name', e.target.value);
}
}
}),
h('span', {}, this.list[index].name)
]);
} */
return h('span', 'item-' + item.data);
},
onInput(newList = []) {
// 组件只负责顺序的变更,其他自定的地方都自己写。 包括内容
const tempList = [];
for (let i = 0, len = newList.length; i < len; i++) {
const item = newList[i];
const id = item[__id__];
tempList.push(this.listMap.get(id));
}
this.list = tempList;
console.log(JSON.parse(JSON.stringify(this.list)));
},
onDelete(deleteData) {
console.log('被删除的对象', deleteData);
},
handleAdd() {
const id = this.__index__.toString();
const dataObj = { data: this.__index__, [__id__]: id };
this.__index__++;
this.list.push(dataObj);
this.listMap.set(id, dataObj);
}
}
};
</script>
<style lang="stylus" scoped></style>