vuedraggable 跨组件的拖拽实现

问题

在 vue 中我们拖拽效果一般都是用的 vuedraggable 组件实现的,但是当一个封装的组件内部用了 vuedraggable,然后想在其他地方用这个组件,要把组件内部的元素拖拽出来放到组件外部的一个 vuedraggable 区域,就如下图所示:

组件 A 和组件 B 都是已封装好的三方组件,并且内部都是用的 vuedraggable 来实现的拖拽。现在想要把组件 A 里的选项拖拽到组件 B 里面,会发现怎么都不会响应拖拽事件。

分析

vuedraggable 组件要让两块区域间能够相互拖拽,是需要设置相同的 group 的。但是我们已确认组件 A 和 组件 B 里的设置的 group 是完全一样的。但组件 B 就是无法响应拖拽事件。尝试将组件 A 和 组件 B 的代码直接放到一个工程里,而不是以组件的方式引入,发现拖拽功能就能正常使用了。

从上面的尝试分析,可以知道 vuedraggable 应该是没有支持跨组件的使用,或者是不是有什么属性配置了就可以支持跨组件使用了?我去翻看了 vuedraggable 的官方文档,并没有找到相关的说明或配置。但是从官方文档我们可以知道它是基于 Sortable.js 的,那我们继续翻看 Sortable.js 的官方文档。令人失望的是 Sortable.js 官方文档中也没有找到相关有用的信息。

到这里我们要解决这个问题只能去翻它的源码了。翻看 vuedraggable 源码,可以看到其内部其实就是在 mounted 的时候 new 了一个 Sortable 对象:

于是我们找到 Sortable 的构造函数:

js 复制代码
function Sortable(el, options) {
  this.el = el; // root element
  this.options = options = _extends({}, options);
  el[expando] = this;

  // 默认配置
   var defaults = {
     ...
   };

  for (var name in defaults) {
    !(name in options) && (options[name] = defaults[name]);
  }
  
	...
  
  if (options.supportPointer) {
    on(el, 'pointerdown', this._onTapStart);
  } else {
    on(el, 'mousedown', this._onTapStart);
    on(el, 'touchstart', this._onTapStart);
  }

  sortables.push(this.el); // Restore sorting

  ...
}

可以看到它给传进去的元素 el 绑定了 mousedown 事件,处理方法是 _onTapStart,那我们在看下 _onTapStart 方法:

js 复制代码
function _onTapStart(evt) {
  var _this = this,
     el = this.el,
     options = this.options,
     preventOnFilter = options.preventOnFilter,
     type = evt.type,
     touch = evt.touches && evt.touches[0] || evt.pointerType && evt.pointerType === 'touch' && evt,
     target = (touch || evt).target,
     originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0] || evt.composedPath && evt.composedPath()[0]) || target,
     filter = options.filter;

  // 一些对事件参数的判断逻辑
  ...

  // 对 filter 处理
  if (typeof filter === 'function') {
    ...
  } else if (filter) {
    ...
  }

  this._prepareDragStart(evt, touch, target);
}

可以看到它里面最终是调用的 _prepareDragStart 方法:

js 复制代码
function _prepareDragStart(evt, touch, target){
  var _this = this,
      el = _this.el,
      options = _this.options,
      ownerDocument = el.ownerDocument,
      dragStartFn;

  if (target && !dragEl && target.parentNode === el) {
    rootEl = el;
    dragEl = target;
    parentEl = dragEl.parentNode;
    nextEl = dragEl.nextSibling;
    lastDownEl = target;
    activeGroup = options.group;
    Sortable.dragged = dragEl;
    
    dragStartFn = function dragStartFn() {...} // 拖拽开始处理逻辑

    on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent);
    on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent);
    on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent);
    on(ownerDocument, 'mouseup', _this._onDrop);
    on(ownerDocument, 'touchend', _this._onDrop);
    on(ownerDocument, 'touchcancel', _this._onDrop);

    dragStartFn();
  }
}

可以看到它在拖拽开始时,会设置一下 dragEl 为当前的拖拽元素,然后会给 ownerDocument 绑定 dragovermouseup 这两个拖拽移动和放置的事件。ownerDocument 是整个 html 文档,而我们的需求是跨组件,不是跨 iframe,所以到这里还是没有问题的。我们继续看 dragover 的处理函数 nearestEmptyInsertDetectEvent

js 复制代码
function nearestEmptyInsertDetectEvent(evt) {
  if (dragEl) {
    evt = evt.touches ? evt.touches[0] : evt;

    var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY);

    if (nearest) {
      // Create imitation event
      var event = {};

      for (var i in evt) {
        if (evt.hasOwnProperty(i)) {
          event[i] = evt[i];
        }
      }

      event.target = event.rootEl = nearest;
      event.preventDefault = void 0;
      event.stopPropagation = void 0;

      nearest[expando]._onDragOver(event);
    }
  }
}

可以看到它会通过 _detectNearestEmptySortable 方法去取最近的可放置区域。那是不是组件外的 vuedraggable 区域是无法取到的?我们加个断点调试发现确实取出来是空的。那问题肯定就在 _detectNearestEmptySortable 这个方法里面了,我们继续看这个方法:

js 复制代码
function _detectNearestEmptySortable(x, y) {
  var ret;
  sortables.some(function (sortable) {
    var threshold = sortable[expando].options.emptyInsertThreshold;
    if (!threshold || lastChild(sortable)) return;
    var rect = getRect(sortable),
        insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold,
        insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold;

    if (insideHorizontally && insideVertically) {
      return ret = sortable;
    }
  });
  return ret;
}

可以看到它是去遍历了 sortables 这个数组,找到 (x,y) 这个坐标点所在的 sortable 元素。那这个 sortables 又是什么呢?再翻一下 Sortable.js 的源码,发现这是一个内部的全局变量:

并且在构造函数中将当前元素加了进去:

js 复制代码
function Sortable(el, options) {
  this.el = el; // root element
  this.options = options = _extends({}, options);
  el[expando] = this;

	...
  
  sortables.push(this.el); // Restore sorting

  ...
}

从前面分析 vuedraggable 组件的代码可以知道,我们每在页面上方一个 vuedraggable 组件,就会 new 一个 Sortable 实例,那就会往 sortables 这个数组里 push 一个 vuedraggable 组件的 dom 元素。但是为什么在同一个工程中的多个 vuedraggable 组件实例间能共用这个 sortables 数组,而以组件形式引入的就无法共用这 sortables 数组了呢?

其实这就涉及到 ES6 模块引用的作用域问题了。我们通过 import 命令去加载一个模块的时候,是会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。也就是说我们在同一个工程中,所有 import vuedraggable 组件的地方,在执行时都是读取的同一个模块文件,这个模块文件里的全局变量 sortables 就是公用的。

而以组件的方式引入后,组件内部的 vuedraggable 子组件就是内部的了,和外部工程就是隔离开的,所以无法公用。

既然问题找到了,那就要想办法能够让组件外部的 vuedraggable 实例能够注入进来。

解决方案

从上面的代码分析,我们可以知道我们的目标是要将外部的 vuedraggable 实例的 dom 元素注入到我们的组件内的 sortables 数组里。而注入 sortables 数组是要通过 new 一个 Sortable 实例来实现的。但是 Sortable 这个构造函数是在 vuedraggable 组件内部的,外部是无法直接获取到的,那我们要怎么来调用这个 Sortable 构造函数呢?

从上面我们分析 vuedraggable 组件代码时,我们看到它会把创建的 Sortable 实例赋值给 _sortable 变量:

于是我们就可以通过 vuedraggable 实例的 _sortable 属性来获取到 Sortable 实例,从而我们就可以通过 Sortable 实例的原型链获取到它的构造函数。

所以我们的方案就是在组件里对外提供一个注入方法,该方法传入一个外部的 vuedraggable 实例,利用这个 vuedraggable 实例来 new 一个 Sortable 实例出来:

js 复制代码
function addDraggableItem(draggable) {
  // 从外部传进的 draggable 实例里获取到它的 dom 元素和配置
  const el = draggable.$el;
  const options = draggable._sortable.options;

  // 通过组件内部的 draggable 实例获取到 sortable 的构造函数
  const innerDraggable = this.$refs['designArea'];
  const sortableConstructor = innerDraggable._sortable.constructor;

  // 创建 sortable 实例,将外部的 draggable 实例注入进来
  new sortableConstructor(el, options);
}

使用时直接调用 addDraggableItem 这个方法即可:

js 复制代码
// 自己工程中的 vuedraggable 实例
const draggable = this.$refs['draggable'];
// 组件实例
const componentA = this.$refs['componentA'];

// 将自己工程的 vuedraggable 实例注入组件中
componentA.addDraggableItem(draggable);
相关推荐
工业互联网专业16 分钟前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
J不A秃V头A1 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客2 小时前
pinia在vue3中的使用
前端·javascript·vue.js
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
爱喝水的小鼠4 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
小晗同学4 小时前
Vue 实现高级穿梭框 Transfer 封装
javascript·vue.js·elementui
forwardMyLife4 小时前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
计算机学姐5 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea
luoluoal5 小时前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot