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权威指南