[Vue3]自定义指令实现组件元素可拖拽移动

news2024/11/25 7:18:18

实现思路:

元素移动设计思路

1.在光标按下的时刻记录下光标的绝对位置坐标(以视窗左上角为原点)(const {clientX, clientY} = evt

clientX / clientY 事件属性返回当事件被触发时光标指针相对于浏览器页面当前 body 可视区域的x, y坐标。

2.记录此时光标相对目标元素的位置。需要获取目标元素的绝对位置坐标(const {x, y} = el.getBoundingClientRect()),计算并记录光标相对目标元素的相对位置坐标pointerRelativePos

el.getBoundingClientRect() 获取元素的大小及其相对于视口的位置。

3.在光标按下移动过程中监听光标变化的绝对位置坐标(clientX / clientY),并根据光标按下时刻记录的光标相对坐标pointerRelativePos来推算出目标元素此时应该定位的绝对位置坐标,并设置目标元素的样式属性inset为对应的坐标位置。
4.最后在光标解除按压时停止监听光标移动。

上述参数表现如图所示:

事件监听设计思路

1.光标按下的时刻采用pointerdown事件监听,此事件支持PC和移动设备(包括鼠标、触摸点和触摸笔)
2.光标移动事件监听PC端采用pointermove,而移动端采用touchmove监听。至于移动端为什么不采用pointermove,由于本人实践发现有一个很奇怪的bug,即移动端触点在移动过程中会随机触发pointerleave,导致pointermove事件中断(即使是把事件pointermove事件挂载在document上也无法避免,暂时没有找到原因,有大佬了解的还望指点)。故这里监听移动端触点移动事件采用touchmove
3.最后停止监听光标移动在触发pointerup事件时接触对pointermovetouchmove事件的监听。

处理细节

1.当触发目标元素的pointerdown事件后,直接在document而非目标元素上添加pointermove事件处理函数。解决在PC端鼠标移动过快而导致目标元素还没有及时调整样式位置到鼠标所在位置时鼠标就移除了目标元素从而触发pointerleave事件导致pointermove事件中断。在document上添加pointermove事件处理函数就不会发生pointerleave的情况(在移动端无效,上面已经提到)
2.监听目标元素的touchmove事件时最好evt.preventDefault(),可以防止其触发默认页面滚动事件。当然本人在代码中设置了可以选择不这么做,当你真的需要滚动页面而不是移动目标元素时。
3.当触发目标元素的pointerdown事件后,设置了一个计时器,默认超出250ms后执行,在此期间若触发了pointerup事件则取消定时器执行,执行内容主要是监听目标元素click事件的捕获阶段并阻止其向后冒泡。解决了在PC端支持目标元素点击事件的效果。实现效果是当鼠标短点击(按下和提起时间在250ms内)目标元素则会正常触发点击事件,当鼠标长按(按下和提起时间超过250ms)目标元素则不会触发其本身挂载的点击事件处理函数,而是触发元素的拖拽移动行为。当然这里的延迟时间250ms只是默认值,可以自定义。

实现效果:

首先是示例代码demo:

<script setup lang="ts">
const onclick = () => console.log('点击了一下')
</script>

<template><div v-draggable="{ className: 'controller-move' }" class="controller" @click="onclick">拖拽移动</div>
</template>
<style scoped>
.controller {position: fixed;top: 100px;left: 100px;z-index: 100;width: 100px;height: 100px;border: 10px solid;border-radius: 10px;line-height: 100px;text-align: center;
}

.controller-move {transform: scale(1.5);filter: opacity(75%);cursor: move;
}
</style> 

其效果如下:

PC端:

使用方法:

1.在 vue 项目 src 根路径 main.js 对createApp创建的app对象使用app.directive('draggable', draggable);挂载全局指令。
2.在 vue 文件的 template 中使用 v-draggable 来挂载该指令在目标元素上
3.指令绑定值传参:即v-draggable=“value”,value中的传参格式如下,也可以什么参数都传。

* {
* device?: 'mobile' | 'pc';设备,传参: 'mobile' | 'pc' | undefined
* ms?: number; 延迟时间, default 250. 与 click 事件区分
* o?: Origin;移动时的相对原点,支持4种, 0: 左上角; 1: 左下角; 2: 右下角; 3: 右上角; 默认为 0: 左上角
* axes?: 'x' | 'y';移动轴,传参: 'x' | 'y' | undefined, 默认 undefined, 即光标位置; 可选择只沿x轴移动或只沿y轴移动
* style?: Partial<CSSStyleDeclaration>;移动过程中 el 的 style 样式, !important: 不要在移动过程中的样式中设置与位置有关的样式属性,如:position、inset、top、left、right、bottom
* className?: string;移动过程中 el 的 class 样式
* setPassive?: () => boolean;设置 touchmove 事件的 passive 属性, 默认 undefined , 阻止 touchmove 默认事件(页面滚动); 若为 true , 不阻止默认事件, 页面可滚动
* onMove?: (el: HTMLElement) => void;开始移动时触发的回调
* onStop?: (el: HTMLElement) => void;停止移动时触发的回调
* } 

以下摘取部分v-draggable指令实现源码:

/**
 * @title draggable
 * @description 使元素可拖拽移动,支持PC、移动端设备(vue directive)
 * @author wzdong
 * @param el 目标元素,目标元素必须是支持 inset 布局,建议 position: fixed
 * @param binding 绑定对象
 */

import { DirectiveBinding } from 'vue';

const draggable = ( el: HTMLElement,{value: {device,ms = 250,o: origin = Origin['topLeft'],axes,style,className,setPassive,onMove,onStop,} = {} as any,}: DirectiveBinding<BindingValue> ) => {... ...let pointerRelativePos: { x: number; y: number },widthHeight: {offsetWidth: number;offsetHeight: number;innerWidth: number;innerHeight: number;},timer: number;// 获取元素宽高以及视窗宽高const getWidthHeight = () => {const { offsetWidth, offsetHeight } = el;const { innerWidth, innerHeight } = window;widthHeight = { offsetWidth, offsetHeight, innerWidth, innerHeight };};getWidthHeight();// 记录指针相对元素位置const recordPointerPos = (clientX: number, clientY: number) => {const { x, y } = el.getBoundingClientRect();pointerRelativePos = {x: clientX - x,y: clientY - y,};};const insetStyle = Array(4).fill('auto');// 设置目标元素位置,以指针为基点const setElPos = (clientX: number, clientY: number) => {const { x, y } = pointerRelativePos;const left = clientX - x;const top = clientY - y;const { offsetWidth, offsetHeight, innerWidth, innerHeight } =widthHeight;const insetAllStyle = `${top}px ${innerWidth - offsetWidth - left}px ${innerHeight - offsetHeight - top}px ${left}px`.split(' ');... ...el.style.inset = insetStyle.join(' ');};// 移动中// 适用于PC, 移动设备 touch 会不定时触发 pointerleave, 无法用 onpointermove 监听const onPointermove = (evt: MouseEvent) => {const { clientX, clientY } = evt;setElPos(clientX, clientY);};// 适用于移动设备const onTouchmove = (evt: TouchEvent) => {// 阻止触摸页面滑动!setPassive?.() && evt.preventDefault();el.removeEventListener('pointermove', onPointermove);const { clientX, clientY } = evt.touches[0];setElPos(clientX, clientY);};// 开始移动const onPointerdown = (evt: PointerEvent) => {const { clientX, clientY } = evt;recordPointerPos(clientX, clientY);getWidthHeight();device !== 'pc' &&el.addEventListener('touchmove', onTouchmove, {passive: !!setPassive?.(),capture: true,});device !== 'mobile' &&document.addEventListener('pointermove', onPointermove, true);timer = setTimeout(() => {onMove?.(el);setStyle(el);// pc端支持长按click事件,因此这里要判断超出 ms 时长即判定为拖拽而非 click// 捕获阶段阻止冒泡,中断之后的事件流el.addEventListener('click',(evt: MouseEvent) => {evt.stopPropagation();},{ capture: true, once: true });}, ms);el.addEventListener('pointerup', stopMove, { once: true });};el.addEventListener('pointerdown', onPointerdown, true);// 停止移动const stopMove = () => {clearTimeout(timer);document.removeEventListener('pointermove', onPointermove, true);el.removeEventListener('touchmove', onTouchmove, true);onStop?.(el);setStyle(el, true);};
};

export default { mounted: draggable }; 

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/102169.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

flutter系列之:移动端手势的具体使用

文章目录简介赋予widget可以点击的功能会动的组件可删除的组件总结简介 之前我们介绍了GestureDetector的定义和其提供的一些基本的方法&#xff0c;GestureDetector的好处就是可以把任何一个widget都赋予类似button的功能。 今天将会通过几个具体的例子来讲解一下GestureDet…

用ChatGPT写一段嵌入式代码

已剪辑自: https://mp.weixin.qq.com/s/uKkUwXx32LPkUYQK44z1lw 废话不多说&#xff0c;开整&#xff01; ChatGPT: Optimizing Language Models for Dialogue&#xff0c;即优化对话的语言模型&#xff0c;它以对话的方式进行交互。对话形式使ChatGPT能够回答后续问题&#…

性能测试---LoadRunner

目录 1.LoadRunner对比Jmeter的优势 2.LoadRunner三个组件之间的关系 3.学习VUG的使用 3.1创建性能测试脚本并进行录制 第一步:打开VUG,创建一个新的性能测试的脚本 第二步:对新建的脚本进行设置 第三步:启动WebTours服务 第四步:回到VUG中,点击录制按钮并设置录制选项…

学习编程的五个关键点!你需要get它,并运用!

总体来说&#xff0c;学习如何编程是一件较难的事情。我最近发现大学里的计算机课程和各种编程训练营错过了编程的一些重要因素&#xff0c;对新手的教学用了不太恰当的方法。于是&#xff0c;我准备分享一个成功的编程课程应该具备的五大基本支柱。 菜鸟的目标是掌握编程的基…

form表单发送put、delete、patch请求的实现过程

关于发送put、delete、patch请求底层实现过程 对于put这些请求&#xff0c;我们无法直接通过form表单发送&#xff0c;form表单仅支持get和post请求&#xff1b; 虽然我们无法直接通过form表单发送这些请求&#xff0c;但我们可以以form表单为载体做二次请求&#xff1a;使用f…

[附源码]计算机毕业设计Node.js宠物商店管理系统(程序+LW)

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

48数据流中的中位数 49表达式 50两数之和

48数据流中的中位数 第一次没看到要求排序&#xff0c;还以为题目答案写错了&#xff0c;用排序的内置函数也正好是nlogn import java.util.ArrayList;public class Solution {ArrayList<Integer> list new ArrayList<>();public void Insert(Integer num) {list…

小满nestjs(第二十七章 nestjs typeOrm关系)

在我们开始的过程中&#xff0c;肯定不会把数据存在一个表里面&#xff0c;我们会进行分表&#xff0c;把数据分开存&#xff0c;然后通过关联关系&#xff0c;联合查询。 typeOrm 文档 一对一 | TypeORM 中文文档 前端代码还是复用上一章的 增加了一个添加Tag <template…

腾讯安全联合发布《2022游戏安全白皮书》:外挂对抗仍然激烈

2022年以来&#xff0c;各类游戏安全事件的发生给不断影响着游戏生态的健康发展。同时&#xff0c;随着游戏行业数字化进程的加快&#xff0c;以及游戏全球化布局的不断推进&#xff0c;游戏厂商对于游戏安全的投入越来越大&#xff0c;掌握最新的行业安全态势有利于其安全防护…

外汇天眼:WiKiEXPO亮相香港亚洲博览馆,史上最强大咖阵容坐镇

凛冬已至&#xff0c;在这个寒冷的冬天&#xff0c;WikiGlobal将于2022年12月16日至17日早9:00--晚18:00在香港的亚洲国际博览馆举办为期两天的“Wiki Finance EXPO Asia 2022”。目前展会已拉开帷幕。  此次展会展厅面积高达5000多平方米&#xff0c;经过WiKiEXPO科学的规划和…

【数据结构】线性表之单链表

目录 一、链表的概念 1、概念 2、分类 3、重点 二、单链表模拟实现 1、准备 2、头插法 3、尾插法 4、指定下标插入 5、遍历 6、删除第一次出现的元素key 7、删除所有的key 8、双指针删除所有的key 一、链表的概念 1、概念 是一种物理存储结构上非连续的存储结构&a…

PS-历史记录

目录 哪里能找到【历史记录】面板 1、窗口→历史记录 2、编辑→清理→历史记录 还原 1、点击【历史记录】面板 快捷键 【ctrlz】 【shiftctrlz】 从当前状态创建新文档 创建新快照 给快照起名 1、右击你要创建快照的步骤 2、点击面板菜单 3、先按住alt不动&#…

Java 对象和类

Java作为一种面向对象语言。支持以下基本概念&#xff1a; 多态继承封装抽象类对象实例方法重载 本节我们重点研究对象和类的概念。 对象&#xff1a;对象是类的一个实例&#xff08;对象不是找个女朋友&#xff09;&#xff0c;有状态和行为。例如&#xff0c;一条狗是一个对…

猿如意|chat GPT测评

文章目录猿如意猿如意传送门猿如意个人使用感受好的一面&#xff1a;可以改进的一面:什么是猿如意chat GPT测评chat GPT使用过程使用场景描述&#xff1a;问题1问题2问题3问题4&#xff1a;问题5&#xff1a;主观感受&#xff1a;认为此功能不足的地方&#xff1a;对此功能的期…

学习编程的过程中可能会走哪些弯路,有哪些经验可以参考?

很多人学习编程, 走的弯路可以总结为以下几点: 一言不合找视频&#xff0c;几十集视频刷半年。 很多人学习编程的时候&#xff0c;喜欢看视频学&#xff0c;我这里总结一下看视频学习编程的弊端。 1. 完善的视频资源往往稍稍过时&#xff0c;比如你会发现很多java的教学视频…

产品设计市场调研有哪些特点?

产品市场种类繁多&#xff0c;变化无常&#xff0c;消费者需求各异。在工业设计之初&#xff0c;需要对行业和区域环境进行调查分析&#xff0c;深入了解市场情况、市场供求关系、客户引导、趋势等&#xff0c;客观合理地对新产品进行适当定位。只有有了正确的新产品概念规划方…

三方接口签名验签简易设计与实现

本人水平有限&#xff0c;对密码学的理解相当浅显。错误与疏漏&#xff0c;欢迎各位指正。 〇、写在前面 接口安全防护是个永恒的话题&#xff0c;提供给前端的接口需要登录&#xff0c;提供给服务的接口(下文简称"三方接口")也需要鉴权&#xff1b;当前大环境下,ht…

chatgpt教我内存对齐,对齐了但没完全对齐?

文章目录内存对齐关于chatgpt的回答总结内存对齐 关于chatgpt的回答 我与chatgpt的对话如下&#xff1a; 我现在来描述与总结上述对话都干了啥以及我为什么要问这个。 我本来是在学习rapidjson源码里面的内存池实现&#xff0c;然后 RAPIDJSON_ALIGN 没有看懂&#xff0c;所…

JSP课设:家庭相册管理系统(附源码+调试)

JSP家庭相册管理系统 &#xff08;1&#xff09;登录模块&#xff1a;分为普通用户和管理员两种角色&#xff1b; &#xff08;2&#xff09;普通用户模块&#xff1a;相册管理&#xff1a;用户可以对自己相册进行编辑&#xff0c;可以进行批量删除相册、新增相册、编辑相册以…

【Golang】案例为基浅谈Go的变量与常量

&#x1f4d3;推荐网站(不断完善中)&#xff1a;个人博客 &#x1f4cc;个人主页&#xff1a;个人主页 &#x1f449;相关专栏&#xff1a;CSDN专栏、个人专栏 &#x1f3dd;立志赚钱&#xff0c;干活想躺&#xff0c;瞎分享的摸鱼工程师一枚 &#x1f352;前言 在上一篇文章中…