【sgDrag】自定义组件:基于Vue开发支持批量声明拖拽元素、被碰撞元素,拖拽全过程监听元素碰撞检测并返回拖拽原始元素、克隆元素及其getBoundingClientRect对象和碰撞接触元素数组。

简介: 【sgDrag】自定义组件:基于Vue开发支持批量声明拖拽元素、被碰撞元素,拖拽全过程监听元素碰撞检测并返回拖拽原始元素、克隆元素及其getBoundingClientRect对象和碰撞接触元素数组。



特性

  1. 长按自定义时长(默认300毫秒),生成拖拽元素副本
  2. 拖拽显示抓取手型
  3. 拖拽过程实时侦听判断目标碰撞元素数组中是否有被接触的元素,并返回元素数组和相关.getBoundingClientRect()参数

sgDrag.vue组件源码

<template>
    <div :class="$options.name">
        <img ref="cloneDrag" class="cloneDrag" :src="src" v-if="src" :style="{ ...style, ...customStyle }"
            draggable="false" />
    </div>
</template>
<script>
import $g from "@/js/sg";
import html2canvas from 'html2canvas'; // npm install --save html2canvas
export default {
    name: 'sgDrag',
    data: () => ({
        offset: { x: 0, y: 0, },//偏移量
        style: { top: '0px', left: '0px', },
        src: null,//克隆的元素图片
        cloneDrag: null,//cloneDrag元素
        originDrag: null,//被拖拽的原始元素
        mouseLeaveOriginDrag: false,//当前鼠标已经离开原始元素
        crashDoms: [],//记录碰撞接触的元素
        timeout: null,
    }),
    props: [
        "data",//可以被拖拽的元素数组(必选)
        "targets",//需要被拖拽接触碰撞检测的元素(必选)
        "type",//屏蔽(可选值:crash[碰撞接触]|mouse[鼠标点位接触],默认值:crash)
        "disabled",//屏蔽
        "customStyle",//自定义拖拽过程样式
        "delay",//默认长按0.2秒 
    ],
    watch: {
        data: {
            handler(newValue, oldValue) {
                newValue ? this.__addDragsEvents(newValue) : this.__removeDragsEvents(oldValue);
            }, deep: true, immediate: true,
        },
        disabled: {
            handler(newValue, oldValue) {
                newValue ? this.__removeAllEvents() : this.__addAllEvents();
            }, deep: true, immediate: true,
        },
    },
    destroyed() {
        this.__removeAllEvents();
    },
    methods: {
        __addAllEvents() {
            this.__addDragsEvents(this.data);
        },
        __removeAllEvents() {
            this.__removeWindowEvents();
            this.__removeDragsEvents(this.data);
            this.mouseup_window();
        },
        __addWindowEvents() {
            this.__removeWindowEvents();
            addEventListener('mousemove', this.mousemove_window);
            addEventListener('mouseup', this.mouseup_window);
        },
        __removeWindowEvents() {
            removeEventListener('mousemove', this.mousemove_window);
            removeEventListener('mouseup', this.mouseup_window);
        },
        // 初始化需要拖拽的DIV
        __addDragsEvents(doms) {
            (doms || []).forEach(dom => {
                this.__addDraggedEvents(dom);
            });
        },
        __removeDragsEvents(doms) {
            (doms || []).forEach(dom => {
                this.__removeDraggedEvents(dom);
            });
        },
        __addDraggedEvents(dom) {
            this.__removeDraggedEvents(dom);
            dom.addEventListener('dragstart', this.dragstart);
            dom.addEventListener('mousedown', this.mousedown);
            dom.addEventListener('mouseup', this.mouseup);
            dom.addEventListener('mouseleave', this.mouseleave);
        },
        __removeDraggedEvents(dom) {
            dom.removeEventListener('dragstart', this.dragstart);
            dom.removeEventListener('mousedown', this.mousedown);
            dom.removeEventListener('mouseup', this.mouseup);
            dom.removeEventListener('mouseleave', this.mouseleave);
        },
        dragstart(e) {
            e.stopPropagation(); e.preventDefault(); return false;
        },
        mousedown(e) {
            if (e.button === 2) return this.mouseup_window(e);//点击了鼠标右键
            if (this.disabled) return this.mouseup_window(e);
            this.mouseLeaveOriginDrag = false;
            this.originDrag = e.currentTarget;
            this.timeout = setTimeout(() => {
                this.timeout = null;
                if (this.originDrag) {
                    html2canvas(this.originDrag).then(canvas => {
                        // 在还未生成副本图片的时候就离开了原始元素
                        if (this.mouseLeaveOriginDrag) {
                            this.mouseup_window(e);
                        } else {
                            //只有生成了副本图片才可以开始记录鼠标移动监听
                            this.src = canvas.toDataURL('image/jpeg', 1.0);
                            this.$nextTick(() => {
                                if (this.disabled) return this.mouseup_window(e);
                                this.cloneDrag = this.$refs.cloneDrag;
                                if (this.originDrag) {
                                    let or = this.originDrag.getBoundingClientRect();
                                    this.offset = { x: e.clientX - or.x, y: e.clientY - or.y, };
                                    this.$emit('dragStart', this.getResult(e));
                                    this.mousemove_window(e);
                                    this.__addWindowEvents();
                                }
                            });
                        }
                    });
                }
            }, typeof this.delay === 'undefined' ? 200 : this.delay);
        },
        mouseleave(e) {
            if (this.disabled) return this.mouseup_window(e);
            this.mouseLeaveOriginDrag || this.$emit('dragOut', this.getResult(e));
            this.mouseLeaveOriginDrag = true;
        },
        mouseup(e) {
            this.timeout && (clearTimeout(this.timeout), this.timeout = null);
        },
        mousemove_window(e) {
            if (this.cloneDrag) {
                this.cloneDrag.setAttribute("dragging", "true"); //拖拽过程变成虚线的样子
                let x = e.clientX - this.offset.x;
                let y = e.clientY - this.offset.y;
                this.style = { left: x + 'px', top: y + 'px', };
                this.$nextTick(() => {
                    // 当新碰撞接触的对象数组和上次碰撞接触的对象数组不同的时候执行
                    this.crashDoms = this.getCrashDoms(e);
                    this.$emit('dragging', this.getResult(e));
                });
            }
        },
        mouseup_window(e) {
            this.$emit('dragEnd', this.getResult(e));
            this.offset = null;
            this.style = null;
            this.src = null;
            this.originDrag && this.originDrag.removeAttribute('draggable');
            this.originDrag = null;
            this.mouseLeaveOriginDrag = false;
            this.crashDoms = [];
            this.__removeWindowEvents();
        },
        getResult(e) {
            return {
                $event: e,
                originDrag: this.originDrag,
                originDragRect: this.originDrag ? this.originDrag.getBoundingClientRect() : null,
                cloneDrag: this.cloneDrag,
                cloneDragRect: this.cloneDrag ? this.cloneDrag.getBoundingClientRect() : null,
                crashDoms: this.crashDoms,
            }
        },
        // 获取碰撞的DOM
        getCrashDoms(e) {
            let r = [];
            let targets = this.targets;
            targets['item'] && (targets = [].slice.call(targets));//NodeList对象
            Array.isArray(targets) || (targets = [targets]);//单个元素处理成数组
            if (targets && targets.length) {
                switch (this.type) {
                    case 'mouse'://鼠标点位接触
                        r = targets.filter(target => $g.isMouseInTarget(target, e))
                        break;
                    case 'crash'://碰撞接触
                    default:
                        r = targets.filter(target => $g.isCrash(target, this.cloneDrag))
                }
            }
            return r;// 获取被碰撞接触的内容
        },
    }
};
</script>
<style lang="scss" scoped>
.sgDrag {
    pointer-events: none;
    .cloneDrag {
        z-index: 999999; //根据情况自己拿捏
        position: fixed;
        left: 0;
        top: 0;
        pointer-events: auto;
        cursor: grab;
        &:active {
            cursor: grabbing;
            opacity: 0.618;
        }
        &[dragging] {
            cursor: grabbing;
            opacity: 0.9;
            border: 1px dashed #409EFF;
            transform: translate(-3px, -3px);
            box-shadow: 5px 10px 0 rgba(0, 0, 0, 0.05);
        }
    }
}
</style>

应用组件

<template>
    <div class="sg-body">
        <sgDrag :data="data" :targets="targets" @dragStart="dragStart" @dragging="dragging" @dragOut="dragOut"
            @dragEnd="dragEnd" :customStyle="{
                'box-color': '#409EFF',
                'box-shadow': '0 12px 22px 0 #409EFF'
            }" />
        <div class="drag-container">
            <div class="title">从此处拖拽出去</div>
            <div class="drag-div" :ref="`drag-div${i}`" v-for="(a, i) in items" :key="i" :index="i">{{ a.label }}</div>
        </div>
        <div class="drag-in-container drag-container" ref="drag-in-container">
            <div class="title">放入此处</div>
            <div class="drag-div" :ref="`drag-div${i}`" v-for="(a, i) in dragInItems" :key="i" :index="i">{{ a.label }}
            </div>
        </div>
    </div>
</template>
<script>
import sgDrag from "@/vue/components/sgDrag";
export default {
    components: { sgDrag },
    data: () => ({
        data: [],//可以被拖拽的元素数组
        targets: [],//需要被拖拽接触碰撞检测的元素
        checkboxGroupValue: [],
        items: [...Array(20)].map((v, i) => ({ label: '显示文本' + i, value: i })),
        dragInItems: [],//动态拖拽进入的元素列表
    }),
    mounted() {
        this.data = [...Array(20)].map((v, i) => this.$refs[`drag-div${i}`][0]);
        this.targets = [this.$refs['drag-in-container']];
        console.log(this.data)
    },
    methods: {
        dragStart(d) {
            // console.log(`拖拽开始`, d);
        },
        dragging(d) {
            // console.log(`拖拽中`, d);
            let crashDoms = d.crashDoms;
            this.targets.forEach(dom => {
                crashDoms.includes(dom) ? dom.setAttribute('active', true) : dom.removeAttribute('active');
            });
        },
        dragOut(d) {
            // console.log(`拖拽的时候鼠标离开元素`, d);
        },
        dragEnd(d) {
            // console.log(`拖拽结束`, d);
            this.targets.forEach(dom => dom.removeAttribute('active'));
            // 如果翻下拖拽那一刻,正好在右侧区域内
            if (d.crashDoms.includes(this.targets[0])) {
                let index = parseInt(d.originDrag.getAttribute('index'));
                this.dragInItems.push(this.items.splice(index, 1)[0]);
                console.log(this.dragInItems, this.items)
            }
        },
    }
};
</script>
<style lang="scss" scoped>
.sg-body {
    position: absolute;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    .title {
        width: 100%;
        height: 50px;
        display: flex;
        justify-content: center;
        align-items: center;
        font-weight: bold;
        font-size: 18px;
    }
    .drag-div {
        width: 100px;
        height: 50px;
        background-color: #409EFF;
        margin: 2px;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    .drag-container {
        width: 300px;
        height: 500px;
        overflow-y: auto;
        background-color: #00000011;
        display: flex;
        flex-wrap: wrap;
        align-content: flex-start;
    }
    .drag-in-container {
        background-color: #409EFF11;
        box-sizing: border-box;
        border: 1px dashed transparent;
        &[active] {
            border-color: #409EFF;
            background-color: #409EFF22;
        }
    }
}
</style>

大量用到了这里的拖拽原生写法

image.png

sgDrag 2.0版本去这里↓

image.png


相关文章
|
17天前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
108 17
|
14天前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
50 1
|
17天前
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
53 0
|
2月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
274 4
|
22天前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
108 21
|
20天前
|
监控 JavaScript 前端开发
Vue 文件批量下载组件封装完整使用方法及优化方案解析
本文详细介绍了批量下载功能的技术实现与组件封装方案。主要包括两种实现方式:**前端打包方案(基于file-saver和jszip)** 和 **后端打包方案**。前者通过前端直接将文件打包为ZIP下载,适合小文件场景;后者由后端生成ZIP文件流返回,适用于大文件或大量文件下载。同时,提供了可复用的Vue组件`BatchDownload`,支持进度条、失败提示等功能。此外,还扩展了下载进度监控和断点续传等高级功能,并针对跨域、性能优化及用户体验改进提出了建议。可根据实际需求选择合适方案并快速集成到项目中。
113 17
|
2月前
|
缓存 JavaScript 前端开发
Vue 基础语法介绍
Vue 基础语法介绍
|
21天前
|
JavaScript 前端开发 UED
Vue 手风琴实现的三种常用方式及长尾关键词解析
手风琴效果是Vue开发中常见的交互组件,可节省页面空间、提升用户体验。本文介绍三种实现方式:1) 原生Vue结合数据绑定与CSS动画;2) 使用Element UI等组件库快速构建;3) 自定义指令操作DOM实现独特效果。每种方式适用于不同场景,可根据项目需求选择。示例包括产品特性页、后台菜单及FAQ展示,灵活满足多样需求。附代码示例与资源链接,助你高效实现手风琴功能。
61 10
|
21天前
|
JavaScript 前端开发 UED
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
64 8
|
17天前
|
JavaScript API 开发者
Vue框架中常见指令的应用概述。
通过以上的详细解析,你应该已经初窥Vue.js的指令的威力了。它们是Vue声明式编程模型的核心之一,无论是构建简单的静态网站还是复杂的单页面应用,你都会经常用到。记住,尽管Vue提供了大量预定义的指令,你还可以创建自定义指令以满足特定的需求。为你的Vue应用程序加上这些功能增强器,让编码变得更轻松、更愉快吧!
29 1