likes
comments
collection
share

HTML5通过api实现拖拽讲解和实例分析

作者站长头像
站长
· 阅读数 26

在现代Web开发中,实现拖拽功能是一项常见而强大的需求。HTML5引入了拖放API(Drag and Drop API),为我们提供了一种简单而高效的方式来实现拖拽操作。其中,e.dataTransfer是该API中的一个重要属性,用于在拖拽操作中传递数据,并控制拖放的效果和行为。本篇博客将深入探索e.dataTransfer的使用,帮助你更好地理解和应用HTML5拖放功能。

1. e.dataTransfer概述

e.dataTransfer是HTML5拖放API中的关键属性,它是一个DataTransfer对象,提供了一组方法和属性,用于在拖拽过程中控制数据传输。通过e.dataTransfer,我们可以设置和获取数据,调整拖放的效果和行为,以及处理文件传输等操作。

2. DataTransfer对象的常用方法和属性

DataTransfer对象提供了几个常用的方法和属性,下面是其中一些重要的:

2.1. setData(format, data)

该方法用于将数据设置到DataTransfer对象中。其中,format参数表示数据的类型,可以是MIME类型或自定义的字符串。data参数是要传输的具体数据值。通过setData方法,我们可以将数据附加到拖拽操作中,以便在目标元素中获取和处理。

2.2. getData(format)

该方法用于从DataTransfer对象中获取指定类型的数据。通过传入format参数,我们可以获取之前设置的特定类型的数据值。在拖放操作的目标元素中,可以使用getData方法来提取源元素设置的数据,进而进行相应的处理。

2.3. clearData(format)

该方法用于从DataTransfer对象中清除指定类型的数据。通过传入format参数,我们可以清除之前设置的特定类型的数据值。这在需要重置或更新数据传输时非常有用。

2.4. dropEffect

dropEffect属性表示拖放操作完成后的效果。可以设置为none、copy、move或link,用于指定拖放操作的行为。通过设置dropEffect,我们可以调整拖放操作在目标位置的表现方式,使用户获得更直观的反馈。

2.5. effectAllowed

effectAllowed属性表示在拖拽过程中允许的操作。可以设置为none、copy、copyLink、copyMove、link、linkMove、move、all或uninitialized。通过设置effectAllowed,我们可以控制拖拽过程中可进行的操作,限制或扩展用户的拖放选项。

2.6. files

files属性是一个FileList对象,表示拖拽操作中传输的文件列表。这在实现文件拖拽上传功能时非常有用。通过files属性,我们可以访问用户拖拽操作中的文件,进行进一步的处理和操作。

3. 实现拖拽功能的步骤

为了更好地理解和应用e.dataTransfer,我们将介绍一般实现拖拽功能的步骤:

3.1. 拖拽源(Drag Source)

首先,我们需要确定要拖拽的元素,即起始位置的元素。以下是拖拽源的基本设置步骤:

  • 为拖拽源元素添加draggable="true"属性,以表示该元素可拖拽。
  • 监听拖拽源元素的dragstart事件,在事件处理函数中使用setData方法将数据设置到DataTransfer对象中。通常,我们会设置数据的类型和具体值,以便在拖放过程中传递和使用这些数据。

3.2. 拖放目标(Drop Target)

接下来,我们需要确定要拖拽到的元素,即目标位置的元素。以下是拖放目标的基本设置步骤:

  • 监听拖放目标元素的dragover事件,使用preventDefault()方法阻止默认行为。这样可以允许元素接受放置操作,并在拖放过程中显示适当的鼠标指针。
  • 监听拖放目标元素的drop事件,在事件处理函数中获取拖拽源元素设置的数据,通常使用getData方法。通过获取数据,我们可以在目标元素中进行相应的处理和操作。

4. 拖拽示例代码

下面是一个简单的拖拽示例代码,演示了如何实现基本的拖拽功能:


<!DOCTYPE html>
<html>
  <head>
    <style>
      .drag-source {
        width: 100px;
        height: 100px;
        background-color: red;
        cursor: move;
      }

      .drop-target {
        width: 200px;
        height: 200px;
        background-color: yellow;
      }
    </style>
  </head>
  <body>
    <div class="drag-source" draggable="true">拖拽我</div>
    <div class="drop-target">拖拽到这里</div>

    <script>
      // 获取拖拽源和拖放目标元素
      const dragSource = document.querySelector('.drag-source');
      const dropTarget = document.querySelector('.drop-target');

      // 拖拽开始时的处理函数
      function handleDragStart(e) {
        e.dataTransfer.setData('text/plain', '拖拽的数据');
      }

      // 拖放目标元素的处理函数
      function handleDragOver(e) {
        e.preventDefault();
      }

      function handleDrop(e) {
        e.preventDefault();
        const data = e.dataTransfer.getData('text/plain');
        console.log('拖拽的数据:', data);
      }

      // 监听拖拽源元素的dragstart事件
      dragSource.addEventListener('dragstart', handleDragStart);

      // 监听拖放目标元素的dragover和drop事件
      dropTarget.addEventListener('dragover', handleDragOver);
      dropTarget.addEventListener('drop', handleDrop);
    </script>
  </body>
</html>

在上述示例中,我们创建了一个红色的拖拽源元素和一个黄色的拖放目标元素。通过设置draggable="true",我们将拖拽源元素设置为可拖拽。通过监听dragstart事件,在拖拽开始时,我们使用setData方法将数据设置为纯文本格式。在拖放目标元素上,我们监听了dragover事件,并使用preventDefault阻止默认行为。这样可以在目标元素上显示合适的鼠标指针,表示可以进行放置操作。最后,在drop事件处理函数中,我们使用getData方法获取拖拽源元素设置的数据,并在控制台中输出。

5.实例演示

例如在下方这个vue的低代码平台项目中的两个相关组件:

<!-- 组件列表 -->
<template>
    <div class="component-list" @dragstart="handleDragStart">
        <div
            v-for="(item, index) in componentList"
            :key="index"
            class="list"
            draggable
            :data-index="index"
        >
            <span v-if="item.icon.substring(0, 2) === 'el'" :class="item.icon">
            
            </span>
            <span v-else class="iconfont" :class="'icon-' + item.icon"></span>
        </div>
    </div>
</template>

<script>
import componentList from '@/custom-component/component-list'

export default {
    data() {
        return {
            componentList,
        }
    },
    methods: {
        // handleDragStart 方法是拖拽开始时的处理函数,通过 e.dataTransfer.setData 将当前拖拽项的索引存储在数据传输对象中。
        handleDragStart(e) {
            e.dataTransfer.setData('index', e.target.dataset.index)
        },
    },
}
</script>

<style lang="scss" scoped>
.component-list {
    height: 65%;
    padding: 10px;
    display: grid;
    grid-gap: 10px 19px;
    grid-template-columns: repeat(auto-fill, 80px);
    grid-template-rows: repeat(auto-fill, 40px);

    .list {
        width: 80px;
        height: 40px;
        border: 1px solid #ddd;
        cursor: grab;
        text-align: center;
        color: #333;
        padding: 2px 5px;
        display: flex;
        align-items: center;
        justify-content: center;

        &:active {
            cursor: grabbing;
        }

        .iconfont {
            margin-right: 4px;
            font-size: 20px;
        }

        .icon-wenben,
        .icon-biaoge {
            font-size: 18px;
        }

        .icon-tupian {
            font-size: 16px;
        }
    }
}
</style>

<template>
    <div class="home">
        <!-- 上方操作栏 -->
        <Toolbar />
        <!-- 下方主体部分 -->
        <main>
            <!-- 左侧组件列表 -->
            <section class="left">
                <!-- 左侧上方可构建组件列表 -->
                <ComponentList />
                <!-- 左侧下方已构建组件列表 -->
                <RealTimeComponentList />
            </section>
            <!-- 中间画布 -->
            <section class="center">
                <div
                    class="content"
                    @drop="handleDrop"
                    @dragover="handleDragOver"
                    @mousedown="handleMouseDown"
                    @mouseup="deselectCurComponent"
                >
                    <Editor />
                </div>
            </section>
            <!-- 右侧属性列表 -->
            <section class="right">
                <el-tabs v-if="curComponent" v-model="activeName">
                    <el-tab-pane label="属性" name="attr">
                        <component :is="curComponent.component + 'Attr'" />
                    </el-tab-pane>
                    <el-tab-pane label="动画" name="animation" style="padding-top: 20px;">
                        <AnimationList />
                    </el-tab-pane>
                    <el-tab-pane label="事件" name="events" style="padding-top: 20px;">
                        <EventList />
                    </el-tab-pane>
                </el-tabs>
                <CanvasAttr v-else></CanvasAttr>
            </section>
        </main>
    </div>
</template>

<script>
import Editor from '@/components/Editor/index'
import ComponentList from '@/components/ComponentList' // 左侧列表组件
import AnimationList from '@/components/AnimationList' // 右侧动画列表
import EventList from '@/components/EventList' // 右侧事件列表
import componentList from '@/custom-component/component-list' // 左侧列表数据
import Toolbar from '@/components/Toolbar'
import { deepCopy } from '@/utils/utils'
import { mapState } from 'vuex'
import generateID from '@/utils/generateID'
import { listenGlobalKeyDown } from '@/utils/shortcutKey'
import RealTimeComponentList from '@/components/RealTimeComponentList'
import CanvasAttr from '@/components/CanvasAttr'
import { changeComponentSizeWithScale } from '@/utils/changeComponentsSizeWithScale'
import { setDefaultcomponentData } from '@/store/snapshot'

export default {
    components: { Editor, ComponentList, AnimationList, EventList, Toolbar, RealTimeComponentList, CanvasAttr },
    data() {
        return {
            activeName: 'attr',
            reSelectAnimateIndex: undefined,
        }
    },
    computed: mapState([
        'componentData',
        'curComponent',
        'isClickComponent',
        'canvasStyleData',
        'editor',
    ]),
    created() {
        this.restore()
        // 全局监听按键事件
        listenGlobalKeyDown()
    },
    methods: {
        restore() {
            // 用保存的数据恢复画布
            if (localStorage.getItem('canvasData')) {
                setDefaultcomponentData(JSON.parse(localStorage.getItem('canvasData')))
                this.$store.commit('setComponentData', JSON.parse(localStorage.getItem('canvasData')))
            }

            if (localStorage.getItem('canvasStyle')) {
                this.$store.commit('setCanvasStyle', JSON.parse(localStorage.getItem('canvasStyle')))
            }
        },

        handleDrop(e) {
            e.preventDefault()
            e.stopPropagation()
            /*
            e.preventDefault()是一个事件方法,用于阻止事件的默认行为。在上下文中,它被用于拖放事件中的handleDrop方法。
            通过调用e.preventDefault(),可以防止浏览器对拖放操作的默认行为进行处理,例如在某些情况下打开链接或显示拖放图标。
            e.stopPropagation()也是一个事件方法,用于停止事件的进一步传播。在上下文中,它被用于拖放事件中的handleDrop方法。
            通过调用e.stopPropagation(),可以防止事件继续向上级元素进行传播,确保事件只在当前元素上进行处理,而不会传递给父级元素或其他监听相同事件的元素。
            */
            // console.log(this.editor)
            const index = e.dataTransfer.getData('index') // 获取拖放数据的索引
            const rectInfo = this.editor.getBoundingClientRect() // 获取编辑器的位置信息
            if (index) {
                const component = deepCopy(componentList[index]) // 深拷贝组件列表中的组件
                component.style.top = e.clientY - rectInfo.y // 设置组件的垂直位置
                component.style.left = e.clientX - rectInfo.x // 设置组件的水平位置
                component.id = generateID() // 生成组件的唯一ID

                // 根据画面比例修改组件样式比例 https://github.com/woai3c/visual-drag-demo/issues/91
                changeComponentSizeWithScale(component) // 根据画布比例调整组件的大小

                this.$store.commit('addComponent', { component }) // 将组件添加到组件列表中
                this.$store.commit('recordSnapshot') // 记录快照,用于撤销和重做功能
            }
        },

        handleDragOver(e) {
            e.preventDefault()
            e.dataTransfer.dropEffect = 'copy'
        },

        handleMouseDown(e) {
            e.stopPropagation()
            this.$store.commit('setClickComponentStatus', false)
            this.$store.commit('setInEditorStatus', true)
        },

        deselectCurComponent(e) {
            if (!this.isClickComponent) {
                this.$store.commit('setCurComponent', { component: null, index: null })
            }

            // 0 左击 1 滚轮 2 右击
            if (e.button != 2) {
                this.$store.commit('hideContextMenu')
            }
        },
    },
}
</script>

<style lang="scss">
.home {
    height: 100vh;
    background: #fff;

    main {
        height: calc(100% - 64px);
        position: relative;

        .left {
            position: absolute;
            height: 100%;
            width: 200px;
            left: 0;
            top: 0;

            & > div {
                overflow: auto;

                &:first-child {
                    border-bottom: 1px solid #ddd;
                }
            }
        }

        .right {
            position: absolute;
            height: 100%;
            width: 288px;
            right: 0;
            top: 0;

            .el-select {
                width: 100%;
            }
        }

        .center {
            margin-left: 200px;
            margin-right: 288px;
            background: #f5f5f5;
            height: 100%;
            overflow: auto;
            padding: 20px;

            .content {
                width: 100%;
                height: 100%;
                overflow: auto;
            }
        }
    }

    .placeholder {
        text-align: center;
        color: #333;
    }

    .global-attr {
        padding: 10px;
    }
}
</style>

在模板部分,有一个中间的画布区域(section class="center"),它包含一个具有拖拽功能的div元素(class="content")。这个div元素绑定了一些拖拽相关的事件处理函数:

  • @drop="handleDrop":当拖拽的元素被放置在这个div元素上时,会触发handleDrop方法。
  • @dragover="handleDragOver":当有元素拖动到这个div元素上方时,会触发handleDragOver方法。
  • @mousedown="handleMouseDown":当鼠标按下时,会触发handleMouseDown方法。
  • @mouseup="deselectCurComponent":当鼠标松开时,会触发deselectCurComponent方法。

在方法部分,定义了这些拖拽相关的事件处理函数:

  • handleDrop(e):处理拖放事件的方法。在这个方法中,首先调用了e.preventDefault()和e.stopPropagation(),阻止了默认的拖放行为和事件传播。然后获取拖放数据的索引,根据索引获取相应的组件数据。通过一些计算,设置了组件的位置、样式等信息,并将组件添加到组件列表中。
  • handleDragOver(e):处理拖拽元素在目标区域上方拖动时的事件。在这个方法中,调用了e.preventDefault(),阻止了默认的拖放行为。并设置了e.dataTransfer.dropEffect属性为'copy',表示可以复制拖拽的元素。
  • handleMouseDown(e):处理鼠标按下事件的方法。在这个方法中,调用了e.stopPropagation(),阻止了事件的进一步传播,并修改了一些状态值。
  • deselectCurComponent(e):处理鼠标松开事件的方法。在这个方法中,根据鼠标点击位置和状态值,进行了一些判断和操作,如隐藏上下文菜单等。

整体来说,这段代码实现了一个拖拽功能的编辑器界面,用户可以从左侧的组件列表中拖拽组件到中间的画布区域,实现组件的添加和位置调整。这些操作通过调用相应的方法,并在方法中处理拖放事件和修改相关数据来实现。

6. 总结

通过e.dataTransfer属性,我们可以在HTML5拖放API中实现数据的传输和拖拽操作的控制。DataTransfer对象提供了setData、getData、clearData等方法,用于设置、获取和清除数据。此外,dropEffect、effectAllowed和files等属性可以帮助我们调整拖放的效果和行为,实现更灵活和功能丰富的拖拽功能。