老项目重构,其中有一些拖拽功能,不过用的是两个开源 JS 拖拽文件实现的效果,版本太老了,所以需要换代了,然后就查阅了能够用 Vue 来简单快速实现拖拽的功能实现方法 :
目录
一、HTML 拖放
首先我们可以使用 HTML 5 本身自带的 拖放 功能 :
官方文档 : HTML5 拖放
HTML 拖放实例
下列是关于拖放的简单例子:
实例 :
<!DOCTYPE HTML> <html> <head> <script> function allowDrop(ev) { ev.preventDefault(); } function drag(ev) { ev.dataTransfer.setData("text", ev.target.id); } function drop(ev) { ev.preventDefault(); var data = ev.dataTransfer.getData("text"); ev.target.appendChild(document.getElementById(data)); } </script> </head> <body> <div id="div1" ondrop="drop(event)" ondragover="allowDrop(event)"></div> <img id="drag1" src="img_logo.gif" draggable="true" ondragstart="drag(event)" width="336" height="69"> </body> </html>
它也许看上去有点复杂,不过让我们研究一下拖放事件的所有不同部分。
把元素设置为可拖放
首先:为了把一个元素设置为可拖放,请把 draggable 属性设置为 true:
<img draggable="true">
拖放的内容 - ondragstart 和 setData()
然后,规定当元素被拖动时发生的事情。
在上面的例子中,ondragstart 属性调用了一个 drag(event) 函数,规定拖动什么数据。
dataTransfer.setData() 方法设置被拖动数据的数据类型和值:
function drag(ev) { ev.dataTransfer.setData("text", ev.target.id); }
在本例中,数据类型是 "text",而值是这个可拖动元素的 id ("drag1")。
拖到何处 - ondragover
ondragover 事件规定被拖动的数据能够被放置到何处。
默认地,数据/元素无法被放置到其他元素中。为了实现拖放,我们必须阻止元素的这种默认的处理方式。
这个任务由 ondragover 事件的 event.preventDefault() 方法完成:
event.preventDefault()
进行放置 - ondrop
当放开被拖数据时,会发生 drop 事件。
在上面的例子中,ondrop 属性调用了一个函数,drop(event):
function drop(ev) { ev.preventDefault(); var data = ev.dataTransfer.getData("text"); ev.target.appendChild(document.getElementById(data)); }
代码解释:
- 调用 preventDefault() 来阻止数据的浏览器默认处理方式( drop 事件的默认行为是以链接形式打开)
- 通过 dataTransfer.getData() 方法获得被拖的数据。该方法将返回在 setData() 方法中设置为相同类型的任何数据
- 被拖数据是被拖元素的 id ("drag1")
- 把被拖元素追加到放置元素中
二、Vue.Draggable ( 强烈推荐 )
查阅各类网站文档后,决定试一下 Vue.Draggable
vue.draggable中文文档 :
Vue.Draggable 是一款基于 Sortable.js 实现的 Vue 拖拽插件。支持移动设备、拖拽和选择文本、智能滚动,可以在不同列表间拖拽、不依赖 jQuery 为基础、Vue 2 过渡动画兼容、支持撤销操作。
1.在项目中总会遇见一些需要排序的数据 , 我们可以通过 vue.draggable 进行拖动排序 。
2.Draggable 为基于 Sortable.js 的 vue 组件,用以实现拖拽功能。
3.拖顶的数据和 data 里的数据为双向绑定 ,在界面变的时候 data 中的数据也在跟着变化。
安装
npm i -S vuedraggable
使用
页面引入
import draggable from "vuedraggable"
定义组件
components: {
draggable
},
定义参数
data() {
return {
drag: false,
syllable: [
{
title: '第 1 组'
},
{
title: '第 2 组'
},
{
title: '第 3 组'
},
]
}
}
页面使用
<draggable
class="syllable_ul"
element="ul"
:list="syllable"
:options="{group:'title', animation:150}"
:no-transition-on-drag="true"
@change="change"
@start="start"
@end="end"
:move="move"
>
<transition-group type="transition" :name="!drag? 'syll_li' : null" :css="true">
<li v-for="(item , idx) in syllable" :key="idx">{{item.title}}</li>
</transition-group>
</draggable>
事件
// evt 里面有两个值,一个 evt.added 和 evt.removed
// 可以分别知道移动元素的 ID 和 删除元素的 ID
change(evt) {
console.log(evt , 'change...')
},
// start , end , add , update , sort , remove 得到的都差不多
start(evt) {
this.drag = true
console.log(evt , 'start...')
},
end(evt) {
console.log(evt , 'end....')
this.drag = true
evt.item // 可以知道拖动的本身
evt.to // 可以知道拖动的目标列表
evt.from // 可以知道之前的列表
evt.oldIndex // 可以知道拖动前的位置
evt.newIndex // 可以知道拖动后的位置
},
move(evt, originalEvent) {
console.log(evt , 'move')
console.log(originalEvent) // 鼠标位置
}
文章 强烈 推荐 =>
记录Vue.Draggable拖拽组件的使用历程
https://blog.csdn.net/weixin_58099
三、vue-grid-layout
( 二 )最近在完成 web 端在线绘图功能时,需要开发一个从左侧拖拽一种图标到画布中。调研了非常多种现在做拖拽布局的组件,调研的地址是:
经过比较,选择了一款优秀的可拖拽框架,vue-grid-layout 。
npm 安装
npm install vue-grid-layout --save
使用 demo
<grid-layout
:layout.sync="layout"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:margin="[10, 10]"
:use-css-transforms="true"
>
<grid-item v-for="item in layout"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:key="item.i">
{{item.i}}
</grid-item>
</grid-layout>
data() {
return {
layout: [
{"x":0,"y":0,"w":2,"h":2,"i":"0"},
{"x":2,"y":0,"w":2,"h":4,"i":"1"},
{"x":4,"y":0,"w":2,"h":5,"i":"2"},
{"x":6,"y":0,"w":2,"h":3,"i":"3"},
{"x":8,"y":0,"w":2,"h":3,"i":"4"},
{"x":10,"y":0,"w":2,"h":3,"i":"5"},
{"x":0,"y":5,"w":2,"h":5,"i":"6"},
{"x":2,"y":5,"w":2,"h":5,"i":"7"},
{"x":4,"y":5,"w":2,"h":5,"i":"8"},
{"x":6,"y":3,"w":2,"h":4,"i":"9"},
{"x":8,"y":4,"w":2,"h":4,"i":"10"},
{"x":10,"y":4,"w":2,"h":4,"i":"11"},
{"x":0,"y":10,"w":2,"h":5,"i":"12"},
{"x":2,"y":10,"w":2,"h":5,"i":"13"},
{"x":4,"y":8,"w":2,"h":4,"i":"14"},
{"x":6,"y":8,"w":2,"h":4,"i":"15"},
{"x":8,"y":10,"w":2,"h":5,"i":"16"},
{"x":10,"y":4,"w":2,"h":2,"i":"17"},
{"x":0,"y":9,"w":2,"h":3,"i":"18"},
{"x":2,"y":6,"w":2,"h":2,"i":"19"}
];
}
}
文档
属性参数说明
GridLayout
layout
- type:
Array
- required:
true
数据源。值必须为
Array
,其数据项为Object
。 每条数据项必须有i
,x
,y
,w
和h
属性。 请参考下面的GridItem
。responsiveLayouts
- type:
Object
- required:
false
- default:
{}
如果
responsive
设置为true
,该配置将作为栅格中每个断点的初始布局。键值是断点名称,每项的值都是类似layout
属性定义的数据结构,值必须为Array
,其数据项为Object
。例如:{lg: [layout items], md: [layout items]}
。需要注意的是,在创建栅格布局后设置该属性无效。colNum
- type:
Number
- required:
false
- default:
12
定义栅格系统的列数,其值需为自然数。
rowHeight
- type:
Number
- required:
false
- default:
150
每行的高度,单位像素。
maxRows
- type:
Number
- required:
false
- default:
Infinity
定义最大行数。
margin
- type:
Array
- required:
false
- default:
[10, 10]
定义栅格中的元素边距。
值必须是包含两个
Number
的数组,数组中第一个元素表示水平边距,第二个表示垂直边距,单位为像素。isDraggable
- type:
Boolean
- required:
false
- default:
true
标识栅格中的元素是否可拖拽。
isResizable
- type:
Boolean
- required:
false
- default:
true
标识栅格中的元素是否可调整大小。
isMirrored
- type:
Boolean
- required:
false
- default:
false
标识栅格中的元素是否可镜像反转。
autoSize
- type:
Boolean
- required:
false
- default:
true
标识容器是否自动调整大小。
verticalCompact
- type:
Boolean
- required:
false
- default:
true
标识布局是否垂直压缩。
useCssTransforms
- type:
Boolean
- required:
false
- default:
true
标识是否使用CSS属性
transition-property: transform;
。responsive
- type:
Boolean
- required:
false
- default:
false
标识布局是否为响应式。
breakpoints
- type:
Object
- required:
false
- default: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
为响应式布局设置断点,其中参数代表不同设备的宽度:lg(large),md(medium),sm(small),xs(extra small)。
cols
- type:
Object
- required:
false
- default: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }
设置每个断点对应的列数。
useStyleCursor
- type:
Boolean
- required:
false
- default:
true
标识是否使用动态鼠标指针样式。当拖动出现卡顿时,将此值设为
false
也许可以缓解布局问题。preventCollision
- type:
Boolean
- default:
false
值设置为ture时,栅格只能拖动至空白处。
GridItem
i
- type:
String
- required:
true
栅格中元素的ID。
x
- type:
Number
- required:
true
标识栅格元素位于第几列,需为自然数。
y
- type:
Number
- required:
true
标识栅格元素位于第几行,需为自然数。
w
- type:
Number
- required:
true
标识栅格元素的初始宽度,值为
colWidth
的倍数。h
- type:
Number
- required:
true
标识栅格元素的初始高度,值为
rowHeight
的倍数。minW
- type:
Number
- required:
false
- default:
1
栅格元素的最小宽度,值为
colWidth
的倍数。如果
w
小于minW
,则minW
的值会被w
覆盖。minH
- type:
Number
- required:
false
- default:
1
栅格元素的最小高度,值为
rowHeight
的倍数。如果
h
小于minH
,则minH
的值会被h
覆盖。maxW
- type:
Number
- required:
false
- default:
Infinity
栅格元素的最大宽度,值为
colWidth
的倍数。如果
w
大于maxW
,则maxW
的值会被w
覆盖。maxH
- type:
Number
- required:
false
- default:
Infinity
栅格元素的最大高度,值为
rowHeight
的倍数。如果
h
大于maxH
,则maxH
的值会被h
覆盖。isDraggable
- type:
Boolean
- required:
false
- default:
null
标识栅格元素是否可拖拽。如果值为
null
则取决于父容器。isResizable
- type:
Boolean
- required:
false
- default:
null
标识栅格元素是否可调整大小。如果值为
null
则取决于父容器。static
- type:
Boolean
- required:
false
- default:
false
标识栅格元素是否为静态的(无法拖拽、调整大小或被其他元素移动)。
dragIgnoreFrom
- type:
String
- required:
false
- default:
'a, button'
标识栅格元素中哪些子元素无法触发拖拽事件,值为
css-like
选择器。请参考 interact.js docs中的
ignoreFrom
。dragAllowFrom
- type:
String
- required:
false
- default:
null
标识栅格元素中哪些子元素可以触发拖拽事件,值为
css-like
选择器。如果值为
null
则表示所有子元素(dragIgnoreFrom
的除外)。请参考 interact.js docs中的
allowFrom
。resizeIgnoreFrom
- type:
String
- required:
false
- default:
'a, button'
标识栅格元素中哪些子元素无法触发调整大小的事件,值为
css-like
选择器。请参考 interact.js docs中的
ignoreFrom
。
限制可拖拽区域
如果我们希望可拖拽的组件中,内部是可以单独点击的,那么需要就拖拽的区域分隔。这里官方提供了参数,可以指定哪些区域是可以拖拽的,哪些区域会被忽略,从而被忽略的区域可以单独进行点击。
<grid-item v-for="item in layout"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
drag-allow-from=".vue-draggable-handle"
drag-ignore-from=".no-drag"
>
<div class="text">
<div class="vue-draggable-handle"></div>
<div class="no-drag">
<span>{{item.i}}</span>
<br/>
<button>click</button>
</div>
</div>
</grid-item>
从外部拖拽组件到画布中
在我们的应用中希望,组件能够从左侧拖到画布中,而再进行拖拽,所以我们在右侧需要先写一个区域是用来被拖拽的。在备选拖拽区域用两个方法来进行触发。
<template>
<div>
<div>
<div class="layoutJSON">
Displayed as <code>[x, y, w, h]</code>:
<div class="columns">
<div class="layoutItem" v-for="item in layout">
<b>{{ item.i }}</b>: [{{ item.x }}, {{ item.y }}, {{ item.w }}, {{ item.h }}]
</div>
</div>
</div>
</div>
<br/>
<div @drag="drag" @dragend="dragend" class="droppable-element" draggable="true"
unselectable="on">Droppable Element (Drag me!)</div>
<div id="content">
<grid-layout ref="gridlayout" :layout.sync="layout"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item :key="item.i" v-for="item in layout"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
>
<span class="text">{{ item.i }}</span>
</grid-item>
</grid-layout>
</div>
</div>
</template>
其他更多的官方说明,请参考
Installation | Vue Grid Layout - ️A grid layout system for Vue.js
<template>
<div>
<div>
<div class="layoutJSON">
Displayed as <code>[x, y, w, h]</code>:
<div class="columns">
<div class="layoutItem" v-for="item in layout">
<b>{{ item.i }}</b>: [{{ item.x }}, {{ item.y }}, {{ item.w }}, {{ item.h }}]
</div>
</div>
</div>
</div>
<br/>
<div @drag="drag" @dragend="dragend" class="droppable-element" draggable="true"
unselectable="on">Droppable Element (Drag me!)</div>
<div id="content">
<grid-layout ref="gridlayout" :layout.sync="layout"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item :key="item.i" v-for="item in layout"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
>
<span class="text">{{ item.i }}</span>
</grid-item>
</grid-layout>
</div>
</div>
</template>
<script>
import {GridLayout, GridItem} from "vue-grid-layout"
let mouseXY = {"x": null, "y": null};
let DragPos = {"x": null, "y": null, "w": 1, "h": 1, "i": null};
export default {
components: {
GridLayout,
GridItem
},
data() {
return {
layout: [
{"x": 0, "y": 0, "w": 2, "h": 2, "i": "0"},
{"x": 2, "y": 0, "w": 2, "h": 4, "i": "1"},
{"x": 4, "y": 0, "w": 2, "h": 5, "i": "2"},
{"x": 6, "y": 0, "w": 2, "h": 3, "i": "3"},
{"x": 8, "y": 0, "w": 2, "h": 3, "i": "4"},
{"x": 10, "y": 0, "w": 2, "h": 3, "i": "5"},
{"x": 0, "y": 5, "w": 2, "h": 5, "i": "6"},
{"x": 2, "y": 5, "w": 2, "h": 5, "i": "7"},
{"x": 4, "y": 5, "w": 2, "h": 5, "i": "8"},
{"x": 5, "y": 10, "w": 4, "h": 3, "i": "9"},
],
}
},
mounted() {
document.addEventListener("dragover", function (e) {
mouseXY.x = e.clientX;
mouseXY.y = e.clientY;
}, false);
},
beforeDestroy() {
},
methods: {
drag: function (e) {
let parentRect = document.getElementById('content').getBoundingClientRect();
let mouseInGrid = false;
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) {
mouseInGrid = true;
}
if (mouseInGrid === true && (this.layout.findIndex(item => item.i === 'drop')) === -1) {
this.layout.push({
x: (this.layout.length * 2) % (this.colNum || 12),
y: this.layout.length + (this.colNum || 12), // puts it at the bottom
w: 1,
h: 1,
i: 'drop',
});
}
let index = this.layout.findIndex(item => item.i === 'drop');
if (index !== -1) {
try {
this.$refs.gridlayout.$children[this.layout.length].$refs.item.style.display = "none";
} catch {
}
let el = this.$refs.gridlayout.$children[index];
el.dragging = {"top": mouseXY.y - parentRect.top, "left": mouseXY.x - parentRect.left};
let new_pos = el.calcXY(mouseXY.y - parentRect.top, mouseXY.x - parentRect.left);
if (mouseInGrid === true) {
this.$refs.gridlayout.dragEvent('dragstart', 'drop', new_pos.x, new_pos.y, 1, 1);
DragPos.i = String(index);
DragPos.x = this.layout[index].x;
DragPos.y = this.layout[index].y;
}
if (mouseInGrid === false) {
this.$refs.gridlayout.dragEvent('dragend', 'drop', new_pos.x, new_pos.y, 1, 1);
this.layout = this.layout.filter(obj => obj.i !== 'drop');
}
}
},
dragend: function (e) {
let parentRect = document.getElementById('content').getBoundingClientRect();
let mouseInGrid = false;
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) {
mouseInGrid = true;
}
if (mouseInGrid === true) {
alert(`Dropped element props:\n${JSON.stringify(DragPos, ['x', 'y', 'w', 'h'], 2)}`);
this.$refs.gridlayout.dragEvent('dragend', 'drop', DragPos.x, DragPos.y, 1, 1);
this.layout = this.layout.filter(obj => obj.i !== 'drop');
// UNCOMMENT below if you want to add a grid-item
/*
this.layout.push({
x: DragPos.x,
y: DragPos.y,
w: 1,
h: 1,
i: DragPos.i,
});
this.$refs.gridLayout.dragEvent('dragend', DragPos.i, DragPos.x,DragPos.y,1,1);
try {
this.$refs.gridLayout.$children[this.layout.length].$refs.item.style.display="block";
} catch {
}
*/
}
},
}
}
</script>
<style scoped>
.droppable-element {
width: 150px;
text-align: center;
background: #fdd;
border: 1px solid black;
margin: 10px 0;
padding: 10px;
}
.vue-grid-layout {
background: #eee;
}
.vue-grid-item:not(.vue-grid-placeholder) {
background: #ccc;
border: 1px solid black;
}
.vue-grid-item .resizing {
opacity: 0.9;
}
.vue-grid-item .static {
background: #cce;
}
.vue-grid-item .text {
font-size: 24px;
text-align: center;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
height: 100%;
width: 100%;
}
.vue-grid-item .no-drag {
height: 100%;
width: 100%;
}
.vue-grid-item .minMax {
font-size: 12px;
}
.vue-grid-item .add {
cursor: pointer;
}
.vue-draggable-handle {
position: absolute;
width: 20px;
height: 20px;
top: 0;
left: 0;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") no-repeat;
background-position: bottom right;
padding: 0 8px 8px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: pointer;
}
.layoutJSON {
background: #ddd;
border: 1px solid black;
margin-top: 10px;
padding: 10px;
}
.layoutJSON {
background: #ddd;
border: 1px solid black;
margin-top: 10px;
padding: 10px;
}
.columns {
-moz-columns: 120px;
-webkit-columns: 120px;
columns: 120px;
}
</style>
【注意 : 其中两个方法应该是会有 bug 】
事件
每一个栅格元素grid-item
上都可以添加监听器,用于监听移动和调整大小事件,这样父级Vue对象就可以收到通知。
<grid-layout
:layout="layout"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:vertical-compact="true"
:margin="[10, 10]"
:use-css-transforms="true"
@layout-created="layoutCreatedEvent"
@layout-before-mount="layoutBeforeMountEvent"
@layout-mounted="layoutMountedEvent"
@layout-ready="layoutReadyEvent"
@layout-updated="layoutUpdatedEvent"
>
<grid-item v-for="item in layout"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:key="item.i"
@resize="resizeEvent"
@move="moveEvent"
@resized="resizedEvent"
@moved="movedEvent">
{{item.i}}
</grid-item>
</grid-layout>
layoutCreatedEvent
对应Vue生命周期的
created
layoutCreatedEvent: function(newLayout){ console.log("Created layout: ", newLayout) }
layoutBeforeMountEvent
对应Vue生命周期的
beforeMount
layoutBeforeMountEvent: function(newLayout){ console.log("beforeMount layout: ", newLayout) }
layoutMountedEvent
对应Vue生命周期的
mounted
layoutMountedEvent: function(newLayout){ console.log("Mounted layout: ", newLayout) }
layoutReadyEvent
当完成mount中的所有操作时生成的事件
layoutReadyEvent: function(newLayout){ console.log("Ready layout: ", newLayout) }
layoutUpdatedEvent
更新事件(布局更新或栅格元素的位置重新计算)
layoutUpdatedEvent: function(newLayout){ console.log("Updated layout: ", newLayout) }
moveEvent
移动时的事件
moveEvent: function(i, newX, newY){ console.log("MOVE i=" + i + ", X=" + newX + ", Y=" + newY); },
resizeEvent
调整大小时的事件
resizeEvent: function(i, newH, newW, newHPx, newWPx){ console.log("RESIZE i=" + i + ", H=" + newH + ", W=" + newW + ", H(px)=" + newHPx + ", W(px)=" + newWPx); },
movedEvent
移动后的事件
movedEvent: function(i, newX, newY){ console.log("MOVED i=" + i + ", X=" + newX + ", Y=" + newY); },
resizedEvent
调整大小后的事件
/** * * @param i the item id/index * @param newH new height in grid rows * @param newW new width in grid columns * @param newHPx new height in pixels * @param newWPx new width in pixels * */ resizedEvent: function(i, newH, newW, newHPx, newWPx){ console.log("RESIZED i=" + i + ", H=" + newH + ", W=" + newW + ", H(px)=" + newHPx + ", W(px)=" + newWPx); },