HTML5拖放API的使用

1,981 阅读4分钟

1、HTML5拖放API

通过拖放API,可以实现网页内容的拖拽。借助拖放API我们可以实现一些实用的功能,比如拖拽文件上传,拼图小游戏。先来看看基本拖放知识和拖放事件

2、有关拖放

在浏览器中所有的元素默认都可以成为被拖动的目标,但是如果直接拖动一个目标,鼠标就会变成“禁用”的状态,这是因为拖动的目标必须要被放置到一个释放区上。简单点来说就是,要想实现就必须有一个拖动的来源目标和拖动的释放区。当一个元素被拖动的时候,会触发以下三个事件

  • drapstart(拖动开始时触发,按下鼠标)
  • drap(拖动时会反复地触发)
  • dragend(拖动结束时触发,鼠标松开)

那如何来创建释放区呢,这就要借助以下四个事件了,只需要绑定前两个事件就可以创建一个释放区

  • dragenter(拖动的目标进入释放区触发)
  • dragover(拖动的目标在释放区移动时触发)
  • drapleave(拖动的目标离开释放区)
  • drop(拖动的目标在释放区释放时触发,也就是在释放区内松开了鼠标)

3、实现基本的拖放

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #src > * {
            float: left;
        }
        #src img{
            width: 100px;
            border: thin solid black;
            margin: 0 15px;
        }
        #target{
            width: 102px;
            height: 102px;
            border: thin solid black;
            box-sizing: border-box;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #target p{
            margin: 0;
            text-align: center;
        }
        #target img{
            border: none;
            margin: 0;
        }
        img.dragged{
            background-color: darkcyan;
            opacity: .5;
        }
        
    </style>
</head>
<body>
    <div id="src">
        <!-- draggable属性可以不设置,默认值是auto,默认所有元素都可拖动 -->
        <img src="./img/apple.jpg" id="apple" alt="">
        <img src="./img/banana.jpg" id="banana" alt="">
        <img src="./img/cherry.jpg" id="cherry" alt="">
        <div id="target" draggable="false">
            <span>Drop Here</span>
        </div>
    </div>

    <script>
        let src = document.getElementById('src')
        let target = document.getElementById('target')
        let msg = document.querySelector('#target span')
        // 拖动的对象
        src.addEventListener('dragstart', function(e) {
      
        })
        
        src.addEventListener('dragend', function(e) {
            msg.innerHTML = 'Drop Here'
        })

        src.addEventListener('drag', function(e) {
            msg.innerHTML = e.target.id
        })

        // 创建释放区
        target.addEventListener('dragenter', function(e) {
            e.preventDefault()
        })
        target.addEventListener('dragover', function(e) {
            e.preventDefault()
        })
    </script>
</body>
</html>

可以看到我们已经实现了一个简单的拖放,拖动水果图片到释放区时鼠标状态时可释放的,这说明释放区已经创建成功了,这里要注意dragenter和dragover事件的默认行为是拒绝接受任何拖放的项目的,所以要使用事件对象阻止默认行为

4、拖放的小案例

通过上面的代码,实现了一个基本的拖放,接下来实现一个完整的案例。大致的功能就是:可以拖动图片到释放区。思路:

  • 首先为拖动对象绑定事件,创建释放区
  • 水果被拖动时,释放区显示水果的id
  • 当水果被拖放到释放区时,将图片克隆一份在释放区显示

附上完整代码:

let src = document.getElementById('src')
let target = document.getElementById('target')
let msg = document.querySelector('#target span')
let dragged = null
src.addEventListener('dragstart', function(e) {
    dragged = e.target
    e.target.classList.add('dragged')
})

src.addEventListener('dragend', function(e) {
    msg.innerHTML = 'Drop Here'
    // 
    Array.from(document.querySelectorAll('.dragged'), function(item) {
        item.classList.remove('dragged')
    })
})

src.addEventListener('drag', function(e) {
    msg.innerHTML = e.target.id
})

// 创建释放区, dragenter和dragover默认不接受任何拖放的项目,所以需要阻止默认行为
target.addEventListener('dragenter', function(e) {
    e.preventDefault()
})
target.addEventListener('dragover', function(e) {
    e.preventDefault()
})
target.addEventListener('dragleave', function(e) {
})
target.addEventListener('drop', function(e) {
    this.innerHTML = ''
    msg.innerHTML = ''
    this.append(dragged.cloneNode(false))
})

最终的效果:

5、拖放与文件上传

拖放API常被用于文件的拖拽,用户可以从本地桌面拖动文件到网页中以实现上传的操作。其实现的基本思路与上面的案例基本一致,只不过还需要利用XMLHttpRequest对象将用户选择的文件上传到服务器。

5.1、DataTransfer对象

要想实现用户拖拽文件上传,第一步就是要考虑如何获取用户拖动的文件。这就需要用到DataTransfer对象了,DataTransfer对象是拖放操作的事件触发时所派发的一个对象,这个对象包含了一些有用的属性和方法,其中就有files属性,这个属性保存的是拖动时产生的文件列表。

5.2、具体实现

file.js:

function post(url, data) {
    return new Promise(function(resolve, reject) {
        const xhr = new XMLHttpRequest()
        xhr.addEventListener('readystatechange', function() {
            if(xhr.readyState === 4) {
                if((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
                    resolve(JSON.parse(xhr.responseText))
                }
            }
        })
        xhr.open('post', url, true)
        // 设置超时时间
        xhr.timeout = 1000
        xhr.addEventListener('timeout', function() {
            throw new Error('请求超时!')
        })
        xhr.send(data)
    })
}

export default class XDfile{
    static defaultOptions = {
        // 文件释放区
        target: document.body || document.documentElement.body,
        // 单击是否可以弹出文件对话框
        clickAddFile: true,
        // 上传路径
        uploadUrl: '',
        // 文件进入释放区添加的类
        dragenterCls: 'dragged',
        // 文件一次性批量添加,true表示允许一次性添加多个文件,false则表示不允许
        multiple: true,
        // 限制文件的类型
        accept: '*',
        // 添加文件后自动上传
        autoUpload: false,
        // 添加文件后的回调函数,参数是添加的文件列表
        addFiles: function(){},
        // 上传操作完成之后的回调函数,参数是服务器返回的结果列表
        uploaded: function(){}
    }

    constructor(options) {
        // 合并配置对象
        this.options = Object.assign(XDfile.defaultOptions, options)
        this.init()
    }

    init() {
        // 文件列表
        this.fileLists = []
        // 添加文件控件
        if(this.options.clickAddFile) {
            this.file = document.createElement('input')
            this.file.type = 'file'
            this.file.accept = this.options.accept
            this.file.style.display = 'none'
            this.file.multiple = this.options.multiple
            this.options.target.appendChild(this.file)
            // 点击拖放区域,相当于点击input文件控件
            this.options.target.addEventListener('click', () => {
                this.file.click()
            })
            // 绑定change事件
            this.file.addEventListener('change', (e) => {
                // 上传文件
                this.options.autoUpload && this.upload(e.target.files)
                this.fileLists.push(...e.target.files)
                this.options.addFiles(e.target.files)
            })
        }

        const _this = this
        // 初始化拖放的释放区
        this.options.target.addEventListener('dragenter', function(e) {
            e.preventDefault()
        })
        this.options.target.addEventListener('dragleave', function(e) {
            _this.options.target.classList.remove(_this.options.dragenterCls)
        })
        this.options.target.addEventListener('dragover', function(e) {
            e.preventDefault()
            _this.options.target.classList.add(_this.options.dragenterCls)
        })
        this.options.target.addEventListener('drop', function(e) {
        	// 用户添加文件时的操作
            e.preventDefault()
            _this.options.target.classList.remove(_this.options.dragenterCls)
            let files = e.dataTransfer.files
            if(!_this.options.multiple && files.length > 1) {
                // 只允许一次性添加一个文件,单文件限制
                console.warn('[XDfile Warn]: 只允许一次性上传或添加一个文件(单文件限制)')
                return
            }
            
            // 上传文件
            _this.options.autoUpload && _this.upload(files)
            _this.fileLists.push(...files)
            _this.options.addFiles(files)
        })
    }

    upload(files) {
        const tasks = []
        for(const file of files) {
            const data = new FormData()
            data.append('file', file)
            data.append('model', 'user')
            data.append('project', 'mos')
            data.append('user', 'admin')
            tasks.push(post(this.options.uploadUrl, data))
        }
        Promise.all(tasks).then((res) => {
            this.options.uploaded(res)
        })
    }
}

file.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="css/iconfont.css">
    <link rel="stylesheet" type="text/css" href="css/index.css"/>
</head>
<body>
	<ul class="file-list">
	</ul>
    <div id="file_target">
        <i class="iconfont icon-yunpanlogo-3"></i>
        <p class="">将文件拖放至此</p>
    </div>
    <div class="upload-info">

    </div>

    <script type="module">
        import XDfile from './js/file.js'
        let xdfile = new XDfile({
            target: document.getElementById('file_target'),
            addFiles: handleAddFiles,
            multiple: true,
            uploadUrl: 'http://localhost:8081/MoFiles/public/upload.php',
            uploaded: handleUploaded,
            progress: function(data) {
                console.log(data)
            },
            autoUpload: true,
            accept: 'image/*'
        })

        function handleAddFiles(files) {
            fileList.push(...files)
            const list = document.querySelector('.file-list')
            let html = ''
            Array.from(files, file => {
                html += template(file)
            })
            list.innerHTML += html
        }

        function handleUploaded(res) {
            const info = document.querySelector('.upload-info')
            let html = ''
            for(let info of res) {
                html += `<p>文件"${info.name}",已经上传成功</p>`
            }
            info.innerHTML += html
        }

        function template(file) {
            let html = ''
            if(file.type.split('/')[0]==='image'){
                  let bloburl = URL.createObjectURL(file)
                  html = `
                  <li class="flie-item" style="background-image:url(${bloburl})" title="${file.name}">
                      <span class="file-name">${file.name}</span>
                  </li>
                  `
            } else {
                  html = `
                  <li class="flie-item" style="background-image:url(./img/file.png)" title="${file.name}">
                      <span class="file-name">${file.name}</span>
                  </li>
                  `
           }
           return html
        }
    </script>
</body>
</html>

最终的效果:

5.3、代码分析

file.js实现了一个简单的文件上传类,这个类负责初始化拖拽区域和添加文件之后的上传操作,支持用户拖拽上传和文件对话框上传。支持单个文件和多个文件添加。init()方法根据配置对象生成做一些初始化操作(初始化拖放区域和是否可以通过文件对话框添加文件,用户添加文件之后调用相应的回调函数),upload()方法上传文件。
file.html导入XDfile这个类,传入相应的配置项即可

6、参考资料

JavaScript高级程序设计(第三版)
HTML权威指南