Vue 拖拽功能 之 自定义指令实现元素拖拽功能

拖拽功能主要操作的是真实的DOM元素以及鼠标事件,在vue中使用自定义指令最合适不过了。 当然使用组件封装起来,然后拖拽这个组件,也是可以实现相同的效果,不过我觉得这样有点大材小用,也缺乏灵活性,好处是可以传入更多的控制属性。总之,在组件与指令之间找到平衡点,自有取舍。

言归正传,要想实现拖拽的效果 要么使用 drag 事件, 要么使用 mouse 鼠标事件,我这里选用的是 mouse 鼠标事件组合。

拖拽元素的主要思路是:

  1. 鼠标按下时(mousedown 事件),记录下坐标,作为开始拖拽的起点
  2. 鼠标移动时(mousemove 事件),用移动后的坐标 与 开始拖拽的起点坐标,计算出移动的距离
  3. 用元素所在位置的坐标,加减移动的距离,则可以计算出元素移动后的位置坐标
  4. 既然已经知道了,元素移动后的位置坐标,那就可以为所欲为了
  5. 最后,鼠标松开时(mouseup 事件),移除监听事件,恢复到初始状态

使用方法:

1. 注册自定义指令 v-draggable, 全局注册使用或者局部注册使用

全局注册

// main.js
import draggable from "./directives/draggable";
Vue.directive("draggable", draggable);

局部注册

// 需要使用的文件
import draggable from "./directives/draggable";
export default {
  data() {
    return {
      // ...
    };
  },
  directives:{ 
    directive
  }
};

2. 简单应用

元素可以随意拖拽,没有边界限制

<div style="position: fixed;" v-draggable></div>

3. 简单应用

元素可以随意拖拽,有边界限制, 无法拖动到屏幕之外, 关键在于 设置 【sticky】 修饰符

<div style="position: fixed;" v-draggable.sticky></div>

4. 自定义应用

拖拽的数据,可以通过 handleDraggable 返回,由你决定数据如何使用, 如何显示

<div style="position: fixed;" v-draggable="handleDraggable"></div>
<script>
export default {
  methods: {
    handleDraggable(config){
      console.log(config)
      if (config.dragging) {
        this.style = {
          left: `${config.x}px`,
          top: `${config.y}px`,
          width: `${config.rect.width}px`,
          height: `${config.rect.height}px`,
        };
      }     
    }
  }
};
</script>

5. 如何区分点击事件

当拖拽的元素,必须需要获取点击事件时,我们可以通过自定义应用的方式实现这一个目标;
一般情况下,我们认为元素没有移动时,鼠标的一次按下与松开,为一次点击事件,故而可以如下处理

handleDraggable(config){
  console.log(config)
  const {type, isMove} = config
  if (type === "mouseup" && !isMove) {
    // 这里为点击事件
    return;
  }
}

6. 返回的数据有哪些

id: 唯一自增id
binding, 自定义指令中的 binding
vnode, 自定义指令中的 vnode
target: 拖拽元素

type: 触发的鼠标事件名称,用于区分不同阶段 mousedown、mousemove、mouseup
rect: 返回getBoundingClientRect()获取拖拽元素的位置

x: 拖拽元素距离屏幕左侧的距离
y: 拖拽元素距离屏幕上方的距离

dragstartX: 鼠标按下时坐标
dragstartY:

dragendX: 鼠标松开时坐标, 无值时为undefined
dragendY:

startX: 拖拽起点坐标
startY:

diffX: 拖拽元素当前移动端额狙击
diffY:

dragging: 元素是否可以拖拽
isMove: 拖拽的元素是否有移动

7. 自定义指令实现元素拖拽功能源码

let seed = 0;
const ctx = "@@draggableContext";
function handleMousedown(event) {
  event.preventDefault();
  const el = this;
  const rect = el.getBoundingClientRect();

  Object.assign(el[ctx], {
    type: "mousedown",
    rect: rect,
    x: rect.x || rect.left,
    y: rect.y || rect.top,
    dragstartX: event.clientX, // 鼠标按下时坐标
    dragstartY: event.clientY,
    dragendX: void 0, // 鼠标松开时坐标
    dragendY: void 0,
    startX: event.clientX, // 起点坐标
    startY: event.clientY,
    dragging: true,
    isMove: false,
  });

  callback(el);

  window.addEventListener("mousemove", el[ctx]._handleMousemove, false);
  window.addEventListener("mouseup", el[ctx]._handleMouseup, false);
}

function handleMousemove(el) {
  return function(event) {
    event.preventDefault();

    if (event.target === document.documentElement) return;

    const current = {
      x: event.clientX,
      y: event.clientY,
    };

    const diff = {
      x: current.x - el[ctx].startX,
      y: current.y - el[ctx].startY,
    };

    if (el[ctx].binding.modifiers.sticky) {
      // 不会拖出屏幕边缘
      const clientWidth = document.documentElement.clientWidth;
      const clientHeight = document.documentElement.clientHeight;

      const {
        x,
        y,
        rect: { width, height },
      } = el[ctx];

      if (diff.x < 0 && x + diff.x <= 0) {
        el[ctx].x = 0;
      } else if (diff.x > 0 && x + width - clientWidth >= 0) {
        el[ctx].x = clientWidth - width;
      } else {
        el[ctx].x += diff.x;
      }

      if (diff.y < 0 && y + diff.y <= 0) {
        el[ctx].y = 0;
      } else if (diff.y > 0 && y + height - clientHeight >= 0) {
        el[ctx].y = clientHeight - height;
      } else {
        el[ctx].y += diff.y;
      }
    } else {
      el[ctx].x += diff.x;
      el[ctx].y += diff.y;
    }

    Object.assign(el[ctx], {
      type: "mousemove",
      startX: current.x,
      startY: current.y,
      diffX: diff.x,
      diffY: diff.y,
      isMove: true,
    });

    callback(el);
  };
}
function handleMouseup(el) {
  return function(event) {
    event.preventDefault();

    const lastType = el[ctx].type;

    Object.assign(el[ctx], {
      type: "mouseup",
      dragendX: event.clientX, // 鼠标按下时坐标
      dragendY: event.clientY,
      dragging: false,
      isMove: lastType === "mousemove",
    });

    callback(el);

    window.removeEventListener("mousemove", el[ctx]._handleMousemove, false);
    window.removeEventListener("mouseup", el[ctx]._handleMouseup, false);
  };
}

function callback(el) {
  const bindingFn = el[ctx]?.binding?.value;
  if (typeof bindingFn === "function") {
    bindingFn({ ...el[ctx], target: el });
  } else {
    const { x, y, rect, dragging } = el[ctx];
    if (!dragging) return;
    el.style.cssText = `
      left: ${x}px;
      top: ${y}px;
      width: ${rect.width}px;
      height: ${rect.height}px;
    `;
  }
}
/**
 * v-draggable
 * @desc
 * @example
 * ```vue
 * <div v-draggable>
 *
 * <div v-draggable.sticky>
 * <div v-draggable="handleDraggable">
 * ```
 */
export default {
  bind(el, binding, vnode) {
    const id = seed++;
    el[ctx] = {
      id,
      binding,
      vnode,
      _handleMousemove: handleMousemove(el, binding, vnode),
      _handleMouseup: handleMouseup(el, binding, vnode),
    };

    el.addEventListener("mousedown", handleMousedown, false);
  },

  unbind(el) {
    window.removeEventListener("mousemove", el[ctx]._handleMousemove, false);
    window.removeEventListener("mouseup", el[ctx]._handleMouseup, false);
    el.removeEventListener("mousedown", handleMousedown, false);
    delete el[ctx];
  },
};

8. 对应移动端代码实现

let seed = 0;
const ctx = "@@draggableContext";
function handleMousedown(event) {
  // event.preventDefault();
  const el = this;
  const rect = el.getBoundingClientRect();

  Object.assign(el[ctx], {
    type: "mousedown",
    rect: rect,
    x: rect.x || rect.left,
    y: rect.y || rect.top,
    dragstartX: event.touches[0].clientX, // 鼠标按下时坐标
    dragstartY: event.touches[0].clientY,
    dragendX: void 0, // 鼠标抬起时坐标
    dragendY: void 0,
    startX: event.touches[0].clientX, // 起点坐标
    startY: event.touches[0].clientY,
    dragging: true,
    isMove: false,
  });

  callback(el);

  window.addEventListener("touchmove", el[ctx]._handleMousemove, false);
  window.addEventListener("touchend", el[ctx]._handleMouseup, false);
}

function handleMousemove(el) {
  return function(event) {
    // event.preventDefault();

    if (event.target === document.documentElement) return;

    const current = {
      x: event.touches[0].clientX,
      y: event.touches[0].clientY,
    };

    const diff = {
      x: current.x - el[ctx].startX,
      y: current.y - el[ctx].startY,
    };

    if (el[ctx].binding.modifiers.sticky) {
      // 不会拖出屏幕边缘
      const clientWidth = document.documentElement.clientWidth;
      const clientHeight = document.documentElement.clientHeight;

      const {
        x,
        y,
        rect: { width, height },
      } = el[ctx];

      if (diff.x < 0 && x + diff.x <= 0) {
        el[ctx].x = 0;
      } else if (diff.x > 0 && x + width - clientWidth >= 0) {
        el[ctx].x = clientWidth - width;
      } else {
        el[ctx].x += diff.x;
      }

      if (diff.y < 0 && y + diff.y <= 0) {
        el[ctx].y = 0;
      } else if (diff.y > 0 && y + height - clientHeight >= 0) {
        el[ctx].y = clientHeight - height;
      } else {
        el[ctx].y += diff.y;
      }
    } else {
      el[ctx].x += diff.x;
      el[ctx].y += diff.y;
    }

    Object.assign(el[ctx], {
      type: "mousemove",
      startX: current.x,
      startY: current.y,
      diffX: diff.x,
      diffY: diff.y,
      isMove: true,
    });

    callback(el);
  };
}
function handleMouseup(el) {
  return function(event) {
    // event.preventDefault();
    const lastType = el[ctx].type;
    Object.assign(el[ctx], {
      type: "mouseup",
      dragendX: el[ctx].startX, // 鼠标按下时坐标
      dragendY: el[ctx].startY,
      dragging: false,
      isMove: lastType === "mousemove",
    });

    callback(el);

    window.removeEventListener("touchmove", el[ctx]._handleMousemove, false);
    window.removeEventListener("touchend", el[ctx]._handleMouseup, false);
  };
}

function callback(el) {
  const bindingFn = el[ctx]?.binding?.value;
  if (typeof bindingFn === "function") {
    bindingFn({ ...el[ctx], target: el });
  } else {
    const { x, y, rect, dragging } = el[ctx];
    if (!dragging) return;
    el.style.cssText = `
      left: ${x}px;
      top: ${y}px;
      width: ${rect.width}px;
      height: ${rect.height}px;
    `;
  }
}
/**
 * v-draggable
 * @desc
 * @example
 * ```vue
 * <div v-draggable>
 *
 * <div v-draggable.sticky>
 * <div v-draggable="handleDraggable">
 * ```
 */
export default {
  bind(el, binding, vnode) {
    const id = seed++;
    el[ctx] = {
      id,
      binding,
      vnode,
      _handleMousemove: handleMousemove(el, binding, vnode),
      _handleMouseup: handleMouseup(el, binding, vnode),
    };

    el.addEventListener("touchstart", handleMousedown, false);
  },
  unbind(el) {
    window.removeEventListener("touchmove", el[ctx]._handleMousemove, false);
    window.removeEventListener("touchstart", el[ctx]._handleMouseup, false);
    el.removeEventListener("touchend", handleMousedown, false);
    delete el[ctx];
  },
};

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

了 义

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值