使用 HTML5 Drag API 实现列表的嵌套拖拽

需求与背景:

使用 HTML5 Drag API 实现列表的嵌套拖拽_第1张图片  

元素列表,多选后可以成组,支持最多3层的嵌套组。

现要求可以拖拽元素,实现顺序的交换、组间的移动;移动时,显示位置提示的样式,放进组内与并列的样式不同;支持多选拖拽。

 

技术选择:HTML5 Drag API

使用的事件:

  • dragstart      被拖动元素
  • dragenter     经过元素
  • dragover      经过元素
  • dragleave    经过元素
  • dragend      被拖动元素

未使用的事件:

  • dragexit      ?
  • drop            经过元素   

 

实现思路:

每个item绑定以上使用的5个事件,item将自己的信息emit到容器内,由容器统一处理。

元素列表的数据结构是tree,每个item除了自己的属性外,有一个数组类型的elements属性,保存的是子元素列表。

在容器内,实现了一些操作tree的工具方法,例如删除节点、移动节点到某个节点后、移动节点到某个节点下等。

 

实现细节:

1.handleDragStart

记录被拖动的元素信息,修改被拖动的元素样式

handleDragStart: function(element, ref) {
      this.isDragging = true
      if(element === 'multi'){
        this.isMultiDragging = true
        this.dragElement = undefined
        this.dragElementIdList = Array.from(this.selectedItemList)
        for(let id of this.dragElementIdList){
          document.getElementById(id).style.opacity = 0.6
        }
        
      } else {
        this.isMultiDragging = false
        this.dragElement = element
        this.dragElementIdList = []
        
        ref.style.opacity = 0.6
      }
    },

2. handleDragEnter

dragenter & dragover 的处理,元素会根据鼠标位置的细微差别,区分目标位置是在自己后、前或是与1、2、3级父节点并列(逻辑细碎不提),函数根据这些信息记录当前目标位置,并显示位置提示样式。

    handleDragEnter: function(element, targetPosition, ref, that) {
      if(!this.isDragging) return;
      
      this.clearStyle('borderBottom')
      this.clearStyle('border')
      let nodeList = this.isMultiDragging ? this.dragElementIdList.map(id => this.getTreeNode(this.elementsTree, id)) : [this.dragElement]
      this.isTargetItemValid = this.isTargetValid(nodeList, element, targetPosition)
      this.targetElementId = this.isTargetItemValid ? element.uuid : undefined
      this.targetPosition = this.isTargetItemValid ? targetPosition : undefined
      let styleColor = this.isTargetItemValid ? '#007FFF' : '#404551'

      //0 后面 
      if(targetPosition===0){
        ref.style.borderBottom = "2px " + styleColor + " inset"
        let pid = this.findDirectParentNode(this.elementsTree, element.uuid)
        if (document.getElementById(pid)) document.getElementById(pid).firstChild.style.border = "1px " + styleColor  + " inset"
      }
      // 1 底层的第一个子节点 放到最前面
      else if(targetPosition===1){
        ref.style.borderTop = "2px " + styleColor  + " inset"
      }
      // 2 第一个子元素
      else if(targetPosition===2){
        ref.style.border = "1px " + styleColor  + " inset"
      }
      // -1 与一级父节点并列
      else if(targetPosition===-1){
        let pid = this.findDirectParentNode(this.elementsTree, element.uuid)
        let realPreNode = this.getTreeNode(this.elementsTree, pid)

        let ppid = this.findDirectParentNode(this.elementsTree, pid)
        let realParentNode = this.getTreeNode(this.elementsTree, ppid)

        if(realParentNode){
          this.targetElementId = realPreNode.uuid
          document.getElementById(realPreNode.uuid).style.borderBottom = "2px " + styleColor  + " inset"
          document.getElementById(realParentNode.uuid).firstChild.style.border = "1px " + styleColor  + " inset"
        } else { // 最外层的最后一个
          let realPreNode = this.elementsTree[this.elementsTree.length - 1]
          this.targetElementId = realPreNode.uuid
          document.getElementById(realPreNode.uuid).style.borderBottom = "2px " + styleColor  + " inset"
        }
      }
      // -2 与二级父节点并列
      else if(targetPosition===-2){
       ...
      }
      // -3 与三级父节点并列
      else if(targetPosition===-3){
       ...
      }

      if(element.type==='GROUP' && targetPosition===0 && nodeList.findIndex(item => item.uuid === element.uuid) < 0){ // 没展开的组,停留一段时间后要展开
        clearTimeout(this.dragTimeout)
        this.dragTimeout = setTimeout(() => {
          if(this.targetElementId===element.uuid){
            that.toggleWrap(null, 1)
          }
        }, 400)
      }
    },

 

3.handleDragEnd

判断移动的合法性、修改tree数据结构、整理数据调用接口通知后端、清楚数据状态与样式。

 handleDragEnd: function() {
      // console.log('from: ', this.dragElement.uuid, 'to: ', this.targetElementId)
      let directParentId = this.findDirectParentNode(this.elementsTree, this.targetElementId)
      if(!this.isTargetItemValid || !this.targetElementId || ( this.dragElement && this.targetElementId === this.dragElement.uuid) || this.dragElementIdList.includes( this.targetElementId)){
        this.clearStyle('border')
        this.clearStyle('borderBottom')
        this.clearStyle('opacity')
        return
      }
      if(this.isDragging){
        let nodeList = this.isMultiDragging ? this.dragElementIdList.map(id => this.getTreeNode(this.elementsTree, id)) : [this.dragElement]
        let nodeIdList = this.isMultiDragging ? this.dragElementIdList : [this.dragElement.uuid]
        // 0 后面
        if(this.targetPosition === 0){
          for(let id of nodeIdList){
            this.deleteTreeNode(this.elementsTree, id)
          }
          this.insertTreeNodeAfter(this.elementsTree, this.targetElementId, nodeList)
          this.dragItemRequest(nodeIdList, '', this.targetElementId) // 需要联调
        }
        // 1 底层的第一个子节点 放到最前面
        else if(this.targetPosition===1){
          for(let id of nodeIdList){
            this.deleteTreeNode(this.elementsTree, id)
          }   
          this.insertTreeNodeUnder(this.elementsTree, undefined, nodeList)
          this.dragItemRequest(nodeIdList, '', '') // 需要联调
        }
        // 2 第一个子元素
        else if(this.targetPosition === 2){
          for(let id of nodeIdList){
            this.deleteTreeNode(this.elementsTree, id)
          }   
          this.insertTreeNodeUnder(this.elementsTree, this.targetElementId, nodeList)
          this.dragItemRequest(nodeIdList, this.targetElementId, '') // 需要联调
        } 
        // -1 -2 -3 此时的targetElementId是preElement的Id
        else if(this.targetPosition < 0){
          for(let id of nodeIdList){
            this.deleteTreeNode(this.elementsTree, id)
          }   
          this.insertTreeNodeAfter(this.elementsTree, this.targetElementId, nodeList)
          this.dragItemRequest(nodeIdList, '', this.targetElementId) // 需要联调
        } 

        this.updateTreeLevel(this.elementsTree)
      }

      this.dragElement = undefined
      this.dragElementIdList = []  
      this.isDragging = false
      this.isMultiDragging = false
      this.targetElementId = ''  
      this.targetPosition = undefined  
      this.clearStyle('border')
      this.clearStyle('borderBottom')
      this.clearStyle('opacity')
    },

坑:

1. drop事件不触发。(在dragover里 preventDefault也不能触发,原因未明,因此用了dragend事件,同样可以实现需求)

2.合法性判断里,要保证移动后嵌套也不能超过3层,因此需要计算depth和level相加,较为麻烦。

3.根据鼠标在元素上dragover时左右位置、上下位置的不同,判断不同的位置,使用event.clientX 与 event.offsetY 参与判断。

 handleDragOver: function(event) {
      event.preventDefault();
      if(this.isGroupLastElement){
        let targetPosition = 0 
        // -1 - 与一级父节点并列  -2 - 与二级父节点并列  -3 - 与三级父节点并列

        if(this.element.type==='GROUP' && !this.isElementsWrap) { // 对于打开的组,只能放进去
          targetPosition = 2 // 第一个子元素
        }
        else if(event.clientX<=60) targetPosition = this.level >= 3 ? -3 : -1 * this.level;
        else if(event.clientX<=90) targetPosition = this.level >= 2 ? -2 : -1 * this.level;
        else if(event.clientX<=120) targetPosition = this.level >= 1 ? -1 : -1 * this.level;

        this.$emit('handle-drag-enter', this.element, targetPosition, this.$refs['self'], this)
      }
      else if(this.level===0 && this.index===0 && (this.element.type !== 'GROUP' || this.isElementsWrap) ){ //底层的第一个子节点 放到最前面
        let targetPosition = 0 
        if(event.offsetY <= 12){
          targetPosition = 1
        }
        this.$emit('handle-drag-enter', this.element, targetPosition, this.$refs['self'], this)
      }

    },

 

使用 HTML5 Drag API 实现列表的嵌套拖拽_第2张图片

你可能感兴趣的:(使用 HTML5 Drag API 实现列表的嵌套拖拽)