实现拖拽

487 阅读3分钟

step1 - pc端-drag

1717834116204.gif

<template>
  <div
    class="item baseBox"
    @dragenter="handleDragenter($event)"
    @dragleave="handleDragleave($event)"
    @dragover="handleDragover($event)"
    @drop="handleDrop"
    :class="isRight ? 'right' : ''"
  >
    {{ fillTitle }}
  </div>
  <div
    class="baseBox baseBox-cursor"
    id="item1"
    @dragstart="handleDragStart($event)"
    @dragend="handleDragend"
    :draggable="true"
  >
    {{ title }}
  </div>
</template>

<script setup>
import { ref } from "vue";
const title = ref("拖拽我");
const fillTitle = ref("");
let draggingId = ref(""); //拖拽对象的id,可通过这种传递信息方式与drop事件连接
let isRight = ref(false); //为true时添加right样式
let targetEl = null; //移入的目标元素

function handleDragStart(ev) {
  draggingId.value = ev.target.id;
}
function handleDragend() {
  if (targetEl) {
    targetEl.classList.remove("dragMoving");
  }
}
function handleDragenter(ev) {
  targetEl = ev.target;
  ev.target.classList.add("dragMoving");
}
function handleDragover(ev) {
  ev.preventDefault(); //允许图标变化
}
function handleDragleave(ev) {
  ev.target.classList.remove("dragMoving");
}
function handleDrop() {
  if (draggingId) {
    isRight.value = true;
    fillTitle.value = title.value;
  }
}
</script>

<style lang="scss" scoped>
.item {
  box-sizing: border-box;
  margin-bottom: 40px;
  cursor: not-allowed;
  &.manager {
    background: #6c63ff;
    color: #ffffff;
    margin-bottom: 63px;
  }
  &.right {
    border: 4px solid #62fb8d;
    color: #62fb8d;
  }
  &.dragMoving {
    border: 4px solid #cad6ce;
    background-color: #cad6ce;
  }
}
.baseBox {
  width: 204px;
  border-radius: 24px;
  height: 56px;
  line-height: 56px;
  font-size: 25px;
  border: 2px solid #6c63ff;
  box-sizing: border-box;
  text-align: center;
  color: #6c63ff;
  user-select: none;
  overflow: hidden;
}
.baseBox-cursor {
  cursor: grab;
  &.selected {
    box-shadow: 0px 0px 7.6px 3px #b2bed6;
  }
  &:hover {
    box-shadow: 0px 0px 7.6px 3px #b2bed6;
  }
  transition: box-shadow 0.5s ease;
}
</style>

step2 - 移动端-没有drag方法,用touchstart/touchmove/touchend

1717836385610.gif

<template>
  <div class="outer">
    <div
      class="item baseBox"
      @dragenter="handleDragenter($event)"
      @dragleave="handleDragleave($event)"
      @dragover="handleDragover($event)"
      @drop="handleDrop"
      :class="isRight ? 'right' : ''"
    >
      {{ fillTitle }}
    </div>
    <div
      class="baseBox baseBox-cursor"
      id="item1"
      @dragstart="handleDragStart($event)"
      @dragend="handleDragend"
      :draggable="true"
      @touchstart="handleTouchstart($event)"
      @touchmove="handleTouchmove($event)"
      @touchend="handleTouchend($event)"
    >
      {{ title }}
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
const title = ref("拖拽我");
const fillTitle = ref("");
let draggingId = ref(""); //拖拽对象的id,可通过这种传递信息方式与drop事件连接
let isRight = ref(false); //为true时添加right样式
let targetEl = null; //移入的目标元素
let cloneNode = null; //克隆的拖拽对象,用于移动

//克隆节点的宽高,用于计算拖拽中心
let width = 0;
let height = 0;

//移动端
function handleTouchstart(ev) {
  ev.preventDefault();
  draggingId.value = ev.target.id;
  targetEl = ev.target;
  //复制节点,用于移动
  cloneNode = targetEl.cloneNode(true);
  cloneNode.style.position = "absolute";
  cloneNode.classList.add("selected");

  //向外部容器添加该节点
  document.getElementsByClassName("app")[0].append(cloneNode);

  height = cloneNode.clientHeight;
  width = cloneNode.clientWidth;
  let { clientX, clientY } = ev.changedTouches[0];
  let x = clientY - height / 2;
  let y = clientX - width / 2;
  cloneNode.style.top = x + "px";
  cloneNode.style.left = y + "px";
}
function handleTouchmove(ev) {
  ev.preventDefault();
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev.targetTouches[0];
  cloneNode.style.top = clientY - height / 2 + "px";
  cloneNode.style.left = clientX - width / 2 + "px";

  //先将样式全部去掉
  document.querySelector(".item").classList.remove("dragMoving");
  //暂时隐藏
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  oElement.classList.add("dragMoving"); //再把到达的添加样式
  //恢复
  cloneNode.style.display = "flex";
}
function handleTouchend(ev) {
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev.changedTouches[0];
  //暂时隐藏,不然oElement获取到的就是cloneNode
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  if (cloneNode) {
    //移除cloneNode
    document.getElementsByClassName("app")[0].removeChild(cloneNode);
  }
  targetEl = oElement;
  targetEl.classList.remove("dragMoving");
  if (draggingId) {
    //这里换成功能的判断逻辑就好了
    isRight.value = true;
    fillTitle.value = title.value;
  }
}

//pc端-drag
function handleDragStart(ev) {
  draggingId.value = ev.target.id;
}
function handleDragend() {
  if (targetEl) {
    targetEl.classList.remove("dragMoving");
  }
}
function handleDragenter(ev) {
  targetEl = ev.target;
  ev.target.classList.add("dragMoving");
}
function handleDragover(ev) {
  ev.preventDefault(); //允许图标变化
}
function handleDragleave(ev) {
  ev.target.classList.remove("dragMoving");
}
function handleDrop() {
  if (draggingId) {
    //这里换成功能的判断逻辑就好了
    isRight.value = true;
    fillTitle.value = title.value;
  }
}
</script>

<style lang="scss" scoped>
.outer {
  height: 100vh;
}
.item {
  box-sizing: border-box;
  margin-bottom: 40px;
  cursor: not-allowed;

  &.manager {
    background: #6c63ff;
    color: #ffffff;
    margin-bottom: 63px;
  }
  &.right {
    border: 4px solid #62fb8d;
    color: #62fb8d;
  }
  &.dragMoving {
    border: 4px solid #cad6ce;
    background-color: #cad6ce;
  }
}
.baseBox {
  width: 204px;
  border-radius: 24px;
  height: 56px;
  line-height: 56px;
  font-size: 25px;
  border: 2px solid #6c63ff;
  box-sizing: border-box;
  text-align: center;
  color: #6c63ff;
  user-select: none;
  overflow: hidden;
}
.baseBox-cursor {
  cursor: grab;
  &.selected {
    box-shadow: 0px 0px 7.6px 3px #b2bed6;
    cursor: grabbing;
  }
  &:hover {
    box-shadow: 0px 0px 7.6px 3px #b2bed6;
  }
  transition: box-shadow 0.5s ease;
}
</style>

step3 - pc端优化-drag方法自带样式问题-选择用原生实现mousedown/mousemove/mouseup-复用移动端逻辑-获取clientX和clientY的地方不一样

image.png

problem1 - mousedown/mousemove/mouseup方法只会在被绑定的元素身上被检测-导致拖拽一卡一卡的

错误代码

image.png

problem2 - mousemove加在最外层盒子上,复制节点cloneNode监听不到move事件-导致拖拽一卡一卡的

错误代码

image.png

更正代码

<template> //只展示新增部分
 <div
      class="baseBox baseBox-cursor"
      id="item1"
      @touchstart="handleTouchstart($event)"
      @touchmove="handleTouchmove($event)"
      @touchend="handleTouchend($event)"
      @mousedown="handleMouseStart($event, index)"
    >
      {{ title }}
    </div>
</template>
<script setup> //只展示新增部分
//pc端
function handleMouseStart(ev) {
  draggingId.value = ev.target.id;
  targetEl = ev.target;
  cloneNode = targetEl.cloneNode(true);
  cloneNode.style.position = "absolute";
  cloneNode.classList.add("selected");

  cloneNode.addEventListener("mousemove", (ev) => { // 直接为节点添加监听事件
    handleMouseMove(ev);
  });

  cloneNode.addEventListener("mouseup", (ev) => { // 直接为节点添加监听事件
    ev.target.classList.remove("selected");
    handleMouseEnd(ev);
  });
  document.getElementsByClassName("app")[0].append(cloneNode);

  height = cloneNode.clientHeight;
  width = cloneNode.clientWidth;
  let { clientX, clientY } = ev;
  let x = clientY - height / 2;
  let y = clientX - width / 2;
  cloneNode.style.top = x + "px";
  cloneNode.style.left = y + "px";
}
function handleMouseMove(ev) {
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev; //ev
  cloneNode.style.top = clientY - height / 2 + "px";
  cloneNode.style.left = clientX - width / 2 + "px";
  document.querySelector(".item").classList.remove("dragMoving");
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  oElement.classList.add("dragMoving");
  cloneNode.style.display = "flex";
}
function handleMouseEnd(ev) {
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev;
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  if (cloneNode) {
    document.getElementsByClassName("app")[0].removeChild(cloneNode);
  }
  targetEl = oElement;
  targetEl.classList.remove("dragMoving");
  if (draggingId) {
    isRight.value = true;
    fillTitle.value = title.value;
  }
}
</script>

image.png

problem2 - 样式问题

image.png

解决

<style lang="scss" scoped> //只展示修改部分
.baseBox {
  width: 204px;
  border-radius: 24px;
  height: 56px;
  line-height: 56px;
  font-size: 25px;
  border: 2px solid #6c63ff;
  box-sizing: border-box;
  text-align: center;
  display: flex; //
  align-items: center; //
  justify-content: center; //
  color: #6c63ff;
  user-select: none;
  overflow: hidden;
}
</style>

step4 - 成功!完成代码!快试试吧!

<template>
  <div class="outer">
    <div class="item baseBox" :class="isRight ? 'right' : ''">
      {{ fillTitle }}
    </div>
    <div
      class="baseBox baseBox-cursor"
      id="item1"
      @touchstart="handleTouchstart($event)"
      @touchmove="handleTouchmove($event)"
      @touchend="handleTouchend($event)"
      @mousedown="handleMouseStart($event, index)"
    >
      {{ title }}
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
const title = ref("拖拽我");
const fillTitle = ref("");
let draggingId = ref(""); //拖拽对象的id,可通过这种传递信息方式与drop事件连接
let isRight = ref(false); //为true时添加right样式
let targetEl = null; //移入的目标元素
let cloneNode = null; //克隆的拖拽对象,用于移动

//克隆节点的宽高,用于计算拖拽中心
let width = 0;
let height = 0;

//pc端
function handleMouseStart(ev) {
  draggingId.value = ev.target.id;
  targetEl = ev.target;
  //复制节点,用于移动
  cloneNode = targetEl.cloneNode(true);
  cloneNode.style.position = "absolute";
  cloneNode.classList.add("selected");

  cloneNode.addEventListener("mousemove", (ev) => {
    handleMouseMove(ev);
  });

  cloneNode.addEventListener("mouseup", (ev) => {
    ev.target.classList.remove("selected");
    handleMouseEnd(ev);
  });

  //向外部容器添加该节点
  document.getElementsByClassName("app")[0].append(cloneNode);

  height = cloneNode.clientHeight;
  width = cloneNode.clientWidth;
  let { clientX, clientY } = ev;
  let x = clientY - height / 2;
  let y = clientX - width / 2;
  cloneNode.style.top = x + "px";
  cloneNode.style.left = y + "px";
}
function handleMouseMove(ev) {
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev;
  cloneNode.style.top = clientY - height / 2 + "px";
  cloneNode.style.left = clientX - width / 2 + "px";

  //先将样式全部去掉
  document.querySelector(".item").classList.remove("dragMoving");
  //暂时隐藏
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  oElement.classList.add("dragMoving"); //再把到达的添加样式
  //恢复
  cloneNode.style.display = "flex";
}
function handleMouseEnd(ev) {
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev;
  //暂时隐藏,不然oElement获取到的就是cloneNode
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  if (cloneNode) {
    //移除cloneNode
    document.getElementsByClassName("app")[0].removeChild(cloneNode);
  }
  targetEl = oElement;
  targetEl.classList.remove("dragMoving");
  if (draggingId) {
    //这里换成功能的判断逻辑就好了
    isRight.value = true;
    fillTitle.value = title.value;
  }
}

//移动端
function handleTouchstart(ev) {
  ev.preventDefault();
  draggingId.value = ev.target.id;
  targetEl = ev.target;
  //复制节点,用于移动
  cloneNode = targetEl.cloneNode(true);
  cloneNode.style.position = "absolute";
  cloneNode.classList.add("selected");

  //向外部容器添加该节点
  document.getElementsByClassName("app")[0].append(cloneNode);

  height = cloneNode.clientHeight;
  width = cloneNode.clientWidth;
  let { clientX, clientY } = ev.changedTouches[0];
  let x = clientY - height / 2;
  let y = clientX - width / 2;
  cloneNode.style.top = x + "px";
  cloneNode.style.left = y + "px";
}
function handleTouchmove(ev) {
  ev.preventDefault();
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev.targetTouches[0];
  cloneNode.style.top = clientY - height / 2 + "px";
  cloneNode.style.left = clientX - width / 2 + "px";

  //先将样式全部去掉
  document.querySelector(".item").classList.remove("dragMoving");
  //暂时隐藏
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  oElement.classList.add("dragMoving"); //再把到达的添加样式
  //恢复
  cloneNode.style.display = "flex";
}
function handleTouchend(ev) {
  if (!cloneNode) {
    return;
  }
  let { clientX, clientY } = ev.changedTouches[0];
  //暂时隐藏,不然oElement获取到的就是cloneNode
  cloneNode.style.display = "none";
  let oElement = document.elementFromPoint(clientX, clientY);
  if (cloneNode) {
    //移除cloneNode
    document.getElementsByClassName("app")[0].removeChild(cloneNode);
  }
  targetEl = oElement;
  targetEl.classList.remove("dragMoving");
  if (draggingId) {
    //这里换成功能的判断逻辑就好了
    isRight.value = true;
    fillTitle.value = title.value;
  }
}

//pc端-drag
function handleDragStart(ev) {
  draggingId.value = ev.target.id;
}
function handleDragend() {
  if (targetEl) {
    targetEl.classList.remove("dragMoving");
  }
}
function handleDragenter(ev) {
  targetEl = ev.target;
  ev.target.classList.add("dragMoving");
}
function handleDragover(ev) {
  ev.preventDefault(); //允许图标变化
}
function handleDragleave(ev) {
  ev.target.classList.remove("dragMoving");
}
function handleDrop() {
  if (draggingId) {
    //这里换成功能的判断逻辑就好了
    isRight.value = true;
    fillTitle.value = title.value;
  }
}
</script>

<style lang="scss" scoped>
.outer {
  height: 100vh;
}
.item {
  box-sizing: border-box;
  margin-bottom: 40px;
  cursor: not-allowed;

  &.manager {
    background: #6c63ff;
    color: #ffffff;
    margin-bottom: 63px;
  }
  &.right {
    border: 4px solid #62fb8d;
    color: #62fb8d;
  }
  &.dragMoving {
    border: 4px solid #cad6ce;
    background-color: #cad6ce;
  }
}
.baseBox {
  width: 204px;
  border-radius: 24px;
  height: 56px;
  line-height: 56px;
  font-size: 25px;
  border: 2px solid #6c63ff;
  box-sizing: border-box;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #6c63ff;
  user-select: none;
  overflow: hidden;
}
.baseBox-cursor {
  cursor: grab;
  &.selected {
    box-shadow: 0px 0px 7.6px 3px #b2bed6;
    cursor: grabbing;
  }
  &:hover {
    box-shadow: 0px 0px 7.6px 3px #b2bed6;
  }
  transition: box-shadow 0.5s ease;
}
</style>