Background: Due to the needs of the project, multiple groups of drag and drop assignments are required, and sorting can also be supported
Renderings
I’ve been too busy recently and don’t have time to describe it, so I’ll upload the complete code directly. The Chinese documentation can be found here?
Complete code
<template>
<div class="draggable_box">
<el-row :gutter="15">
<el-col :span="6">
<div></div>
</el-col>
<el-col :span="6">
<div class="area">
<div class="card">
<div class="card_top">
<div class="title">First-level menu options</div>
</div>
<draggable v-model="menus" chosenClass="chosen"
:options="{group:{name: 'first',pull:'clone',put: true},sort: true}" forceFallback="true"
animation="100"
filter=".unmover"
handle=".mover"
@start="onStart" @end="onEndMenu" :move="onMoveMenu">
<transition-group class="menu_area">
<div class="item" :class="element.disabled?'unmover':'mover'" v-for="element in menus"
:key="element.id + '-menus'">{<!-- -->{ element.name }}
</div>
</transition-group>
</draggable>
</div>
<div class="card">
<div class="card_top">
<div class="title">Secondary menu options</div>
</div>
<draggable v-model="submenus" chosenClass="chosen" forceFallback="true"
:options="{group:{name: 'second',pull:'clone',put: true},sort: true}" animation="100"
@start="onStart" @end="onEnd" :move="onMoveSubmenu">
<transition-group class="submenu_area">
<div class="item" v-for="element in submenus" :key="element.id + '-submenus'">{<!-- -->{ element.name }}</div>
</transition-group>
</draggable>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="area card">
<div class="card_top">
<div class="title">Permission assignment results</div>
</div>
<draggable v-model="resultArr" chosenClass="chosen" forceFallback="true"
:group="{name: 'first',pull: true,put: true}" animation="100"
@start="onStart" @end="onEndResultMenu" :move="onMoveResultMenu">
<transition-group class="result_menu_area">
<div v-for="(firstEl,index) in resultArr" :key="firstEl.id + 'firstEl'">
<div class="item" v-if="firstEl.type===1">
<div>{<!-- -->{ firstEl.name }}</div>
<draggable v-model="firstEl.seconds" chosenClass="chosen" forceFallback="true"
:group="{name: 'second',pull: true,put: true,index}"
dataIdAttr="data-id"
animation="100"
@start="onStart" @end="onEndResultSubMenu" :move="onMoveResultSubMenu">
<transition-group class="result_submenu_area" :index="index">
<div class="item" v-for="secondEl in firstEl.seconds" :key="secondEl.id + 'secondEl'">
<div>{<!-- -->{ secondEl.name }}</div>
</div>
</transition-group>
</draggable>
</div>
</div>
</transition-group>
</draggable>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
//Import draggable component
import draggable from 'vuedraggable'
export default {
//Register draggable component
components: {
draggable,
},
data() {
return {
drag: false,
//Define the array of objects to be dragged
menus: [
{id: 1, name: 'First-level menu 1', type: 1, seconds: [], disabled: false},
{id: 2, name: 'First-level menu 2', type: 1, seconds: [], disabled: false},
{id: 3, name: 'First-level menu 3', type: 1, seconds: [], disabled: false},
{id: 4, name: 'First-level menu 4', type: 1, seconds: [], disabled: false}
],
submenus: [
{id: 1, name: 'Second-level menu 1', type: 2},
{id: 2, name: 'Secondary menu 2', type: 2},
{id: 3, name: 'Secondary menu 3', type: 2},
{id: 4, name: 'Second-level menu 4', type: 2}], //empty array
resultArr: [],
style: 'min-height:120px;display: block;',
moveItem: ''
}
},
mounted() {
},
methods: {
//Start drag event
onStart() {
this.drag = true;
},
//Drag end event
onEnd() {
this.drag = false;
},
//move callback method
onMove(e, originalEvent) {
this.moveItem = e.draggedContext.element //Drag object
//false means preventing drag and drop
return true;
},
/**
* Drag-and-drop first-level menu callback event, used to control which element is not allowed to be dragged and obtain the object of the currently dragged element
* @param e //e object structure
* draggedContext: dragged element
* index: the serial number of the dragged element
* element: the object corresponding to the dragged element
* futureIndex: expected position, target position
* relatedContext: the object to be docked
* index: the serial number of the target docking object
* element: the object corresponding to the target element
* list: target array
* component: the vue component object that will be docked
* @param originalEvent
* @return {boolean} false means preventing drag and drop
*/
onMoveMenu(e, originalEvent) {
this.moveItem = e.draggedContext //Drag object
// false means to prevent dragging: repeated dragging is not allowed, and dragging to sub-elements of the same level is not allowed.
return !this.resultArr.some(item => item.id === this.moveItem.element.id) & amp; & amp; originalEvent.rootEl._prevClass === 'result_menu_area';
},
//Last level menu drag end event
onEndMenu(e) {
this.drag = false;
const result = this.resultArr.filter((item) => {
return item.id === this.moveItem.element.id;
})
// The object does not exist, there was one originally, and after adding the new one, there are two.
if (result. length < 2) {
this.$set(this.menus[this.moveItem.index], 'disabled', true)
return;
}
// The object exists, delete the new object
this.resultArr.splice(e.newDraggableIndex, 1)
},
//Drag secondary menu callback event, used to control which element is not allowed to be dragged and obtain the object of the currently dragged element
onMoveSubmenu(e, originalEvent) {
this.moveItem = e.draggedContext //Drag object
// false means to prevent dragging: repeated dragging is not allowed, and dragging to sub-elements of the same level is not allowed.
return !(e.relatedContext.list & amp; & amp; e.relatedContext.list.some(item => item.id === this.moveItem.element.id)) & amp; & amp; originalEvent.rootEl. _prevClass === 'result_submenu_area';
},
// Drag and drop result directory first-level menu callback
onMoveResultMenu(e, originalEvent) {
this.moveItem = e.draggedContext //Drag object
let list = e.relatedContext.list
// false means to prevent dragging: repeated dragging is not allowed, and dragging to sub-elements of the same level is not allowed.
if (e.to._prevClass === 'result_menu_area') {
// Drag to the results area
return !(list & amp; & amp; list.filter(item => item.id === this.moveItem.element.id).length > 2)
} else {
return e.to._prevClass === 'menu_area'
}
},
// Drag-and-drop result directory first-level menu end event
onEndResultMenu(e) {
this.drag = false;
// Determine whether the target area is a first-level menu option
if (e.to._prevClass === 'menu_area') {
let list = [];
// menus array deduplication
this.menus.forEach((item) => {
if (!list.some(el => el.id === item.id)) {
list.push(item)
}
})
this.menus = list;
// Find the object subscript of the original array
this.$nextTick(() => {
this.menus.forEach((item) => {
item.seconds = [];
if (item.id === this.moveItem.element.id) {
item.disabled = false;
}
})
})
}
},
// Drag and drop result directory secondary menu callback
onMoveResultSubMenu(e, originalEvent) {
this.moveItem = e.draggedContext //Drag object
let list = e.relatedContext.list
// false means to prevent dragging: repeated dragging is not allowed, dragging is not allowed under child elements of the same level and cannot be at the same level as the parent element
if (e.to._prevClass === 'result_submenu_area') {
return !(list & amp; & amp; list.filter(item => item.id === this.moveItem.element.id).length > 2)
} else {
return e.to._prevClass === 'submenu_area'
}
},
// End event of the secondary menu of the drag result directory, supporting sorting
onEndResultSubMenu(e) {
this.drag = false;
if (e.to._prevClass === 'result_submenu_area') {
// Get the subscript of the target area
let index = e.to.__vue__.$attrs.index
// Determine whether there is already a drag object in the secondary menu of the target area
const result = this.resultArr[index].seconds.filter(item => item.id === this.moveItem.element.id)
// The object does not exist, there was one originally, and after adding the new one, there are two.
if (result. length < 2) {
return;
}
// The object exists, delete the new object
this.resultArr[index].seconds.splice(e.newDraggableIndex, 1)
} else {
const result = this.submenus.filter((item) => {
return item.id === this.moveItem.element.id;
})
// The object does not exist, there was one originally, and after adding the new one, there are two.
if (result. length < 2) {
return;
}
// The object exists, delete the new object
this.submenus.splice(e.newDraggableIndex, 1)
}
},
}
</script>
<style scoped>
/*Style of the dragged object*/
.item {
padding: 6px;
background-color: #ffffff;
border: solid 2px #ffffff;
margin-bottom: 10px;
cursor: move;
}
.unmover {
background-color: #C0C4CC;
border: solid 2px #C0C4CC;
margin-bottom: 10px;
cursor: not-allowed;
}
/*Select style*/
.chosen {
border: solid 2px #3089dc !important;
}
.draggable_box {
overflow: hidden;
}
.area {
height: 76vh;
display: flex;
flex-direction: column;
gap: 15px;
}
.area > .card {
flex: 1;
}
.card {
background-color: #EBEEF5;
border-radius: 4px;
padding: 15px;
font-size: 14px;
box-sizing: border-box;
}
.card_top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: #111111;
font-size: 16px;
}
.result_menu_area {
display: block;
height: calc(76vh - 71px);
overflow: auto;
}
.result_menu_area::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.result_menu_area::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: #e1e1e1;
}
.result_menu_area::-webkit-scrollbar-track {
border-radius: 6px;
background-color: #f1f1f1;
}
.submenu_area, .result_submenu_area, .menu_area {
display: block;
min-height: 120px;
}
</style>
First introduce a wave of parameters
var sortable = new Sortable(el, {
//Set when there are multiple groups on a web page
//or { name: "...", pull: [true, false, 'clone', array], put: [true, false, array] }
group: "name",
//Whether internal sorting of columns is allowed, if false, when there are multiple sorting groups, dragging can be done between multiple groups, but not itself.
sort: true,
// How long does it take to drag after pressing the mouse? 1000 means 1 second
delay: 0,
//If it is false, the delay will be calculated only when the mouse is pressed and does not move, and it will be invalid if the mouse is moved.
delayOnTouchOnly: false,
//When the mouse is pressed and moved n pixels, the delay event will be canceled, and the element cannot be dragged beyond this range.
//px, how many pixels the point should move before canceling a delayed drag event
touchStartThreshold: 0,
//Enable and disable drag and drop
disabled: false,
//Store
store: null,
//animation effect
animation: 150,
// Easing animation defaults to null. See https://easings.net/ for examples.
easing: "cubic-bezier(1, 0, 0, 1)",
//Handle, click on the object of the specified class style to drag the element
handle: ".my-handle",
//Ignore elements whose class is ignore-elements and cannot be dragged, or use functions to filter objects that are not allowed to be dragged.
// Selectors that do not lead to dragging (String or Function)
filter: ".ignore-elements",
//Calling `event.preventDefault()` when filter is triggered
// Call `event.preventDefault()` when triggered `filter`
preventOnFilter: true,
//Specify which elements can be dragged
// Specifies which items inside the element should be draggable
draggable: ".item",
//Specify to get the data attribute sorted after dragging
dataIdAttr: 'data-id',
//Custom style of docking position
// Class name for the drop placeholder
ghostClass: "sortable-ghost",
//Custom style of selected element
// Class name for the chosen item
chosenClass: "sortable-chosen",
//Custom style when dragging
// Class name for the dragging item
dragClass: "sortable-drag",
//The size of the interactive area, the distance between the A element and the B element to trigger the replacement position
//Threshold of the swap zone
swapThreshold: 1,
// Will always use inverted swap zone if set to true
invertSwap: false,
// Threshold of the inverted swap zone (will be set to swapThreshold value by default)
invertedSwapThreshold: 1,
//Drag direction (the direction will be automatically determined by default)
direction: 'horizontal',
//Ignore HTML5 native drag and drop behavior
forceFallback: false,
//The style name of the cloned element when dragging
// Class name for the cloned DOM Element when using forceFallback
fallbackClass: "sortable-fallback",
// Appends the cloned DOM Element into the Document's Body
fallbackOnBody: false,
// Specify in pixels how far the mouse should move before it's considered as a drag.
fallbackTolerance: 0,
dragoverBubble: false,
// Remove the clone element when it is not showing, rather than just hiding it
removeCloneOnHide: true,
// px, distance mouse must be from empty sortable to insert drag element into it
emptyInsertThreshold: 5,
setData: function (/** DataTransfer */dataTransfer, /** HTMLElement*/dragEl) {
dataTransfer.setData('Text', dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
},
//Click selected element event
// Element is chosen
onChoose: function (/**Event*/evt) {
evt.oldIndex; // element index within parent
},
//Uncheck event
// Element is unchosen
onUnchoose: function (/**Event*/evt) {
// same properties as onEnd
},
//Start drag event
// Element dragging started
onStart: function (/**Event*/evt) {
evt.oldIndex; // element index within parent
},
//End drag event
// Element dragging ended
onEnd: function (/**Event*/evt) {
var itemEl = evt.item; // dragged HTMLElement
evt.to; // target list
evt.from; // previous list
evt.oldIndex; // element's old index within old parent
evt.newIndex; // element's new index within new parent
evt.oldDraggableIndex; // element's old index within old parent, only counting draggable elements
evt.newDraggableIndex; // element's new index within new parent, only counting draggable elements
evt.clone // the clone element
evt.pullMode; // when item is in another sortable: `"clone"` if cloning, `true` if moving
},
// Event when the dragged element is added to other lists
// Element is dropped into the list from another list
onAdd: function (/**Event*/evt) {
// same properties as onEnd
},
//Events when sorting changes
// Changed sorting within list
onUpdate: function (/**Event*/evt) {
// same properties as onEnd
},
// Called by any change to the list (add / update / remove)
onSort: function (/**Event*/evt) {
// same properties as onEnd
},
// Element is removed from the list into another list
onRemove: function (/**Event*/evt) {
// same properties as onEnd
},
// Attempt to drag a filtered element
onFilter: function (/**Event*/evt) {
var itemEl = evt.item; // HTMLElement receiving the `mousedown|tapstart` event.
},
// Event when you move an item in the list or between lists
onMove: function (/**Event*/evt, /**Event*/originalEvent) {
/*
evt.dragged; // The dragged object
evt.draggedRect; // The area where the dragged object is located {left, top, right, bottom}
evt.related; // Replaced object
evt.relatedRect; // DOMRect
evt.willInsertAfter; // Is it in front or behind the object being replaced?
originalEvent.clientY; // mouse position
*/
evt.dragged; // dragged HTMLElement
evt.draggedRect; // DOMRect {left, top, right, bottom}
evt.related; // HTMLElement on which have guided
evt.relatedRect; // DOMRect
evt.willInsertAfter; // Boolean that is true if Sortable will insert drag element after target by default
originalEvent.clientY; // mouse position
// return false; - for cancel
// return -1; - insert before target
// return 1; - insert after target
},
// Called when creating a clone of element
onClone: function (/**Event*/evt) {
var origEl = evt.item;
var cloneEl = evt.clone;
},
// Called when dragging element changes position
onChange: function (/**Event*/evt) {
evt.newIndex // most likely why this event is used is to get the dragging element's current index
// same properties as onEnd
}
});