[vue2 multi-column drag and drop component] vue.draggable nested drag and drop use

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
    }
});