The business scenario needed a tree component that could be dragged to change the location of a node, so I took the opportunity to get my hands on html5 native drag. Recently, I have time to extract the core part of the code, and briefly say how to implement it.

1. Tree structure – Recursive use of components

The tree structure is very simple. The tree component is the parent component. The structure is as follows

tree.vue


<template>
  <div>
    <Tree-Node v-for="item in data" :key="item.title" :node-data="item"></Tree-Node>
  </div>
</template>

Copy the code

Vue components allow themselves to be called in their own templates, so they can form a tree structure where a unique name must be filled in.

tree-node.vue

<template>
  <transition name="slide-up">
    <ul :class="classes">
      <li>
        <div :class="[prefixCls + '-item']">
          <i class="sp-icon sp-icon-arrow-right" :class="arrowClasses" @click.stop="toggleCollapseStatus()"></i>
          <span :class="[prefixCls + '-title-wrap']" ref="dropTarget">
            <span :class="[dragClasses,dragOverClass]" ref="draggAbleDom" v-html="nodeData.title"></span>
          </span>
        </div>
        <Tree-Node v-for="item in nodeData.children" :key="item.title" :node-data="item" v-show="nodeData.children.length && nodeData.isExpand"></Tree-Node>
      </li>
    </ul>
  </transition>
</template>

Copy the code

2. It drag and drop API

1. Draggable specifies whether an element can be dragged. Currently Internet Explorer 9+, Firefox, Opera, Chrome, and Safari support draggable 2

  • Ondragstart: Triggers action on the dragged element when the element starts to be dragged
  • Ondragenter: An event that is triggered when a drag element enters the target element, acting on the target element
  • Ondragover: An event that is triggered when a drag element is moved on the target element, acting on the target element
  • Ondragleave: Triggered when a drag element is dragged away from the target element
  • Ondrop: An event triggered when the mouse is released over the target element while the element is being dragged
  • Ondragend: Event triggered when the drag is complete, applied to the dragged element

3. Drag the node

Define variables

Handling drag nodes requires several key variables

  • The node currently being dragged
  • The node through which the drag occurs
  • The node that is finally placed

So we define an object to hold drag information

dragOverStatus: {
    overNodeKey: "".dropPosition: "".dragNode: {}}Copy the code

Bind drag events

Here, the onDragStart event is bound to the child element and the other events are bound to the parent element, because when testing the real machine IE10, onDragStart and other events are bound to the same element and events such as onDragenter cannot be triggered.

<span :class="[prefixCls + '-title-wrap']" ref="dropTarget">
    <span :class="[dragClasses,dragOverClass]" ref="draggAbleDom" v-html="nodeData.title"></span>
</span>

Copy the code
 mounted() {
    // Bind drag events
    if (this.root.draggable) {
      this.$refs.draggAbleDom.draggable = !this.nodeData.noDrag;
      this.$refs.draggAbleDom.ondragstart = this.onDragStart;

      this.$refs.dropTarget.ondragenter = this.onDragEnter;
      this.$refs.dropTarget.ondragover = this.onDragOver;
      this.$refs.dropTarget.ondragleave = this.onDragLeave;
      this.$refs.dropTarget.ondrop = this.onDrop;
      this.$refs.dropTarget.ondragend = this.onDragEnd; }}Copy the code

When a drag event for a node is triggered, the current node instance can be retrieved from the drag event. HTML5 provides a special drag and drop API, native implementation of complex operations, do not need to use their own mouse event simulation, so the implementation of drag and drop effects is very simple.

(1). Start drag: triggered on the drag element, the event only needs to save the information of the current drag node

onDragStart(e, treeNode) {
      this.dragOverStatus.dragNode = {
        nodeData: treeNode.nodeData,
        parentNode: treeNode.parentNodeData
      };
      this.$emit("on-dragStart", {
        treeNode: treeNode.nodeData,
        parentNode: treeNode.parentNodeData,
        event: e
      });
    }
Copy the code

(2). Enter the target node: triggered on the target element, mainly save the key of the node currently passed, and then send an event to the outer layer for the component caller to do other operations. To avoid frequent events when dragging an element across many nodes, set a timer to trigger after a certain amount of time.

onDragEnter(e, treeNode) {
      // Disable target nodes when no drag node is set
      if (!this.hasDragNode()) {
        return;
      }
      this.dragOverStatus.overNodeKey = "";
      // The drag node is the same as the target node, return
      if (
        treeNode.nodeData._hash === this.dragOverStatus.dragNode.nodeData._hash
      ) {
        return;
      }
      this.dragOverStatus.overNodeKey = treeNode.nodeData._hash; // The key of the deployable node currently passing by
      // The current node cannot be placed as a node
      if (treeNode.nodeData.noDrop) {
        return;
      }
      // Set the dragEnter timer to trigger the event after 250 milliseconds
      if (!this.delayedDragEnterLogic) {
        this.delayedDragEnterLogic = {};
      }
      Object.keys(this.delayedDragEnterLogic).forEach(key= > {
        clearTimeout(this.delayedDragEnterLogic[key]);
      });
      this.delayedDragEnterLogic[
        treeNode.nodeData._hash
      ] = setTimeout((a)= > {
        if(! treeNode.nodeData.isExpand) { treeNode.toggleCollapseStatus(); }this.$emit("on-dragEnter", {
          treeNode: treeNode.nodeData,
          parentNode: treeNode.parentNodeData,
          event: e
        });
      }, 250);
    }
Copy the code

(3). On the target node through: triggered on the target element, real-time calculation of the mouse on the target node position, used to judge the final placement position, 0 (as the target node child node), -1 (placed in front of the target node), 1 (placed behind the target node), display the corresponding style.

onDragOver(e, treeNode) {
      // Disable target nodes when no drag node is set
      if (!this.hasDragNode()) {
        return;
      }
      if (
        this.dragOverStatus.overNodeKey === treeNode.nodeData._hash
      ) {
        this.dragOverStatus.dropPosition = this.calDropPosition(e); // Place identifiers 0, -1,1
      }
      this.$emit("on-dragOver", {
        treeNode: treeNode.nodeData,
        parentNode: treeNode.parentNodeData,
        event: e
      });
      this.dragOverClass = this.setDragOverClass();// Set the mouseover style
    },
Copy the code

When the mouse is above the target node in the target node (1/5), it means to put it in front of the target node – the same level; when the mouse is below the target node in the target node (1/5), it means to put it behind the target node – the same level; otherwise, it is used as the child node of the target node

calDropPosition(e) {
      var offsetTop = this.getOffset(e.target).top;
      var offsetHeight = e.target.offsetHeight;
      var pageY = e.pageY;
      var gapHeight = 0.2 * offsetHeight;
      if (pageY > offsetTop + offsetHeight - gapHeight) {
        // Put it after the target node - sibling
        return 1;
      }
      if (pageY < offsetTop + gapHeight) {
        // Put it in front of the target node - sibling
        return - 1;
      }
      // Put it inside the target node - as a child node
      return 0;
    }
Copy the code

(4). Node placement: triggered on the target element. At this time, the dragged information variable will be used as a parameter to send the event to the outer layer, and the other operations will be decided by the outer layer.

onDrop(e, treeNode) {
      // Disable target nodes when no drag node is set
      if (!this.hasDragNode()) {
        return;
      }
      // When drag is disabled on the current node
      if (treeNode.nodeData.noDrop) {
        return;
      }
      // The drag node is the same as the target node, no operation is done
      if (
        this.dragOverStatus.dragNode.nodeData._hash === treeNode.nodeData._hash
      ) {
        return;
      }

      var res = {
        event: e,
        dragNode: this.dragOverStatus.dragNode,
        dropNode: {
          nodeData: treeNode.nodeData,
          parentNode: treeNode.parentNodeData
        },
        dropPosition: this.dragOverStatus.dropPosition
      };
      this.$emit("on-drop", res);
    }
Copy the code

(5). Drag end: effect on the drag element, after the drag end will clear the variable, restore the style.

onDragEnd(e, treeNode) {
      // Disable target nodes when no drag node is set
      if (!this.hasDragNode()) {
        return;
      }
      // When drag is disabled on the current node
      if (treeNode.nodeData.noDrop) {
        return true;
      }
      this.dragOverStatus.dragNode = null;
      this.dragOverStatus.overNodeKey = "";

      this.$emit("on-dragEnd", {
        treeNode: treeNode.nodeData,
        parentNode: treeNode.parentNodeData,
        event: e
      });
    }
Copy the code

4. The application

Call the tree drag component to obtain the drag node, target node and position in the drag process. The specific drag result is determined by the caller, which can update the tree structure through the call interface, or the front end can process the input data and update the view.

<template>
    <Tree :data="data1" draggable @on-drop="getDropData">
    </Tree>
</template>
Copy the code
getDropData(info) {
      var dragData = info.dragNode.nodeData;
      var dragParent = info.dragNode.parentNode;
      var dropData = info.dropNode.nodeData;
      var dropParent = info.dropNode.parentNode;
      var dropPosition = info.dropPosition; //0 is the child, -1 is placed before the target node, 1 is placed after the target node

      // Remove the drag element from the parent node
      dragParent.children.splice(dragParent.children.indexOf(dragData), 1);
      if (dropPosition === 0) {
        dropData.children.push(dragData);
      } else {
        var index = dropParent.children.indexOf(dropData);
        if (dropPosition === - 1) {
          dropParent.children.splice(index, 0, dragData);
        } else {
          dropParent.children.splice(index + 1.0, dragData); }}}Copy the code

As child nodes, change the hierarchy

Modify the sort to place the drag node after the target node

The source code in this