Drag And Drop
Drag and Drop Scaling A drag-and-drop component that works on both mobile and PC, consisting of two components: Draggable and Droppable.
- Draggable: Represents a draggable element.
- Droppable: Represents an area where draggable elements can be dropped.
To accommodate various use cases:
Draggablecan configure its drag type viatype.Droppablecan configure allowed drag types viaacceptDragType.Droppableonly acceptsDraggableelements whosetypeis included in itsacceptDragType.
Installation
INFO
If you need other components, refer to Full Components Installation
To install this component individually, run:
npm install @havue/drag-and-drop --saveyarn add @havue/drag-and-droppnpm install @havue/drag-and-dropImport
<script>
import { HvDraggable, HvDroppable } from 'havue'
// or
import { HvDraggable, HvDroppable } from '@havue/components'
// or
import { HvDraggable, HvDroppable } from '@havue/drag-and-drop'
</script>To listen to global drag events, you can also import the singleton manager:
import { DnDManagerInstance } from '@havue/drag-and-drop'Example
- Left-side label blocks are Draggable elements, displaying their type and size.
- The gray Draggable element has
immediate="right", allowing instant dragging to the right. - The yellow labels sets the drag-item custom slot display.
- Right-side contains two Droppable areas:
- Green labels can only be dropped in the green area.
- Yellow labels can only be dropped in the yellow area.
Draggable list:
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
immediate: right
Droppable's accept-drag-type property is set to 'green'.
Droppable's accept-drag-type property is set to 'yellow'.
Click to view code
<template>
<div class="dnd-demo" ref="divRef">
<div class="left-box">
<p>Draggable list:</p>
<div class="top-drag-list-box">
<Draggable :type="item.type" v-for="(item, i) in DragItems" :data="item" :key="i" :immediate="item.immediate">
<div class="drag-box" :style="item.style">
{{ item.type }}: {{ `${item.width} * ${item.height}` }}
<br />
{{ item.immediate ? 'immediate: ' + item.immediate : '' }}
</div>
<template #drag-item v-if="item.type === 'yellow'">
<div style="width: 50px; height: 50px; background-color: yellow"></div>
</template>
</Draggable>
</div>
</div>
<div class="right-box">
<p>Droppable's accept-drag-type property is set to 'green'.</p>
<Droppable
accept-drag-type="green"
@enter="
(type: DragAndDropDragType, point: DragAndDropPoint, data: any) => {
enteredType = 'green'
onEnter(type, point, data)
}
"
@move="onMove"
@leave="onLeave"
@drop="onDrop"
>
<div class="drop-box">
<div class="preview-box" v-if="enteredType === 'green'" :style="previewStyle"></div>
<div
class="inner-box-item"
v-for="item in greenInnerBoxList"
:key="item.key"
:style="{
top: item.y,
left: item.x,
width: item.width,
height: item.height
}"
></div>
</div>
</Droppable>
<p>Droppable's accept-drag-type property is set to 'yellow'.</p>
<Droppable
accept-drag-type="yellow"
@enter="
(type: DragAndDropDragType, point: DragAndDropPoint, data: any) => {
enteredType = 'yellow'
onEnter(type, point, data)
}
"
@move="onMove"
@leave="onLeave"
@drop="onDrop"
>
<div class="drop-box yellow">
<div class="preview-box" v-if="enteredType === 'yellow'" :style="previewStyle"></div>
<div
class="inner-box-item"
v-for="item in yellowInnerBoxList"
:key="item.key"
:style="{
top: item.y,
left: item.x,
width: item.width,
height: item.height
}"
></div>
</div>
</Droppable>
</div>
</div>
</template><script setup lang="ts">
import type { DragAndDropPoint, DragAndDropDragType } from '@havue/drag-and-drop'
import { computed, ref, reactive } from 'vue'
// import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/drag-and-drop'
import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/components'
type BoxType = {
key: string | number | symbol
x: string
y: string
width: string
height: string
}
type EnteredType = 'green' | 'yellow' | ''
const divRef = ref<HTMLElement>()
const enteredType = ref<EnteredType>('')
const enteredPoint = ref({
x: 0,
y: 0
})
const previewData = ref()
const DragItems = reactive(
Array(40)
.fill(0)
.map((_, i) => {
let random1 = Math.round(Math.random() * 50)
let random2 = Math.round(Math.random() * 50)
return {
type: random1 % 2 === 0 ? 'green' : 'yellow',
width: random1 + 150,
height: random2 + 50,
immediate: i % 2 == 0 ? ('right' as const) : undefined,
style: {
background: i % 2 == 0 ? 'gray' : ''
}
}
})
)
const greenInnerBoxList = reactive<BoxType[]>([
{
key: 1,
x: '20px',
y: '20px',
width: '20px',
height: '20px'
},
{
key: 2,
x: '100px',
y: '30px',
width: '50px',
height: '200px'
},
{
key: 3,
x: '200px',
y: '40px',
width: '90px',
height: '40px'
}
])
const yellowInnerBoxList = reactive<BoxType[]>([
{
key: 4,
x: '200px',
y: '40px',
width: '90px',
height: '40px'
}
])
const previewStyle = computed(() => {
const preview = previewData.value || {}
return {
top: `${enteredPoint.value.y}px`,
left: `${enteredPoint.value.x}px`,
width: `${preview.width || 100}px`,
height: `${preview.height || 50}px`
}
})
function onEnter(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
enteredPoint.value = point
previewData.value = data
}
function onMove(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
enteredPoint.value = point
previewData.value = data
}
function onLeave() {
enteredType.value = ''
enteredPoint.value = { x: 0, y: 0 }
previewData.value = undefined
}
function onDrop(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
data = data || {}
const { width = 100, height = 50 } = data
if (enteredType.value === 'green') {
greenInnerBoxList.push({
key: Date.now(),
x: `${point.x - width / 2}px`,
y: `${point.y - height / 2}px`,
width: `${width}px`,
height: `${height}px`
})
} else if (enteredType.value === 'yellow') {
yellowInnerBoxList.push({
key: Date.now(),
x: `${point.x - width / 2}px`,
y: `${point.y - height / 2}px`,
width: `${width}px`,
height: `${height}px`
})
}
enteredType.value = ''
enteredPoint.value = { x: 0, y: 0 }
previewData.value = undefined
}
</script><style lang="scss" scoped>
.dnd-demo {
display: flex;
width: 100%;
height: 700px;
overflow: auto;
background-color: rgb(20 177 177 / 41.6%);
p {
margin: 5px;
}
.left-box {
width: 30%;
height: 100%;
margin-right: 15px;
overflow: auto;
background-color: rgb(162 101 22 / 69.4%);
.top-drag-list-box {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: flex-start;
padding: 10px;
.drag-box {
width: 130px;
height: 50px;
color: white;
user-select: none;
background-color: black;
}
}
}
.right-box {
flex: 1;
.drop-box {
position: relative;
width: 400px;
height: 300px;
overflow: hidden;
background-color: rgb(11 104 28);
&.yellow {
background-color: rgb(188 149 21);
}
.preview-box {
position: absolute;
background-color: rgb(127 255 212 / 30%);
transform: translate(-50%, -50%);
}
.inner-box-item {
position: absolute;
background-color: rgb(143 10 65 / 46.6%);
}
}
}
}
</style>Draggable Props
| Property | Description | Type | Default |
|---|---|---|---|
| type | Drag element type | DragAndDropDragType | - |
| immediate? | Direction where the element responds to drag immediately (default requires long-press) | ImmediateType | ImmediateType[] | - |
| disabled? | Whether the element is disabled | boolean | - |
| data? | Data passed to Droppable events (third argument in drop/enter/move/leave) | any | — |
type DragAndDropDragType: string | number | symbol
type ImmediateType = 'left' | 'right' | 'top' | 'bottom' | 'all' | undefinedDraggable Slots
| Slot Name | Description |
|---|---|
| default | Default content |
| drag-item | Element displayed during dragging |
Droppable Props
| Property | Description | Type | Default |
|---|---|---|---|
| acceptDragType | Accepted drag types | DragAndDropDragType | Array<DragAndDropDragType> | - |
| disabled? ^1.2.0 | Whether the element is disabled | boolean | - |
Droppable Events
| Event | Description | Parameter Types |
|---|---|---|
| enter | Triggered when Draggable enters the area | (type: DragType, point: DragAndDropPoint, data: any) => void |
| move | Triggered while Draggable moves within the area | (type: DragType, point: DragAndDropPoint, data: any) => void |
| drop | Triggered when Draggable is dropped | (type: DragType, point: DragAndDropPoint, data: any) => void |
| leave | Triggered when Draggable leaves the area | (type: DragType, data: any) => void |
DragAndDropPoint: {
/** X-coordinate within Droppable */
x: string
/** Y-coordinate within Droppable */
y: string
}Droppable Slots
| Slot Name | Description |
|---|---|
| default | Default content |
Global drag manager DnDManagerInstance ^1.2.2
DnDManagerInstance is a global drag manager based on an internal event bus. It listens to mouse / touch events on document.body, and emits high‑level drag lifecycle events that you can subscribe to from anywhere.
import { onBeforeUnmount, onMounted } from 'vue'
import type { DnDManagerEvents } from '@havue/drag-and-drop'
import { DnDManagerInstance } from '@havue/drag-and-drop'
const handleMove: DnDManagerEvents['move'] = ({ type, data, point }) => {
console.log('move', type, data, point)
}
onMounted(() => {
DnDManagerInstance.on('move', handleMove)
})
onBeforeUnmount(() => {
DnDManagerInstance.off('move', handleMove)
})Event types
type DragAndDropPoint = {
x: number
y: number
}
type DragAndDropDragType = string | number | symbol
type DnDManagerEvents = {
down: (p: DragAndDropPoint) => void
'first-move': (p: DragAndDropPoint, e: MouseEvent | TouchEvent) => void
start: (p: DragAndDropPoint) => void
move: (params: { type: DragAndDropDragType; data: any; point: DragAndDropPoint }) => void
end: (params: { type: DragAndDropDragType; data: any; point: DragAndDropPoint }) => void
}| Event | Description | Parameters |
|---|---|---|
down | Triggered when pointer is pressed down, before drag is started | (point: DragAndDropPoint) |
first-move | First pointer move after down, before drag is confirmed | (point: DragAndDropPoint, e: MouseEvent | TouchEvent) |
start | Drag is confirmed (long‑press or threshold reached) | (point: DragAndDropPoint) |
move | Fired on every drag move while dragging | ({ type: DragAndDropDragType, data: any, point: DragAndDropPoint }) |
end | Fired when drag ends (mouse / touch release, or cancelled) | ({ type: DragAndDropDragType, data: any, point: DragAndDropPoint }) |
DnDManagerInstance events demo
The following demo shows how to subscribe to these events and display them in real time:
Drag the block and observe global events on the right.
DnDManagerInstance events:
Status: idle
Click to view code (DnDManagerInstance demo)
<template>
<div class="dnd-manager-demo">
<div class="left-box">
<p>Drag the block and observe global events on the right.</p>
<Draggable :type="'box'" :data="{ width: 120, height: 60 }">
<div class="drag-source">Drag me</div>
</Draggable>
<Droppable accept-drag-type="box">
<div class="drop-area">Drop area</div>
</Droppable>
</div>
<div class="right-box">
<p>DnDManagerInstance events:</p>
<p class="status">
<span>Status: {{ status }}</span>
<button class="clear-btn" type="button" @click="clearLogs">Clear logs</button>
</p>
<ul class="log-list">
<li v-for="(item, index) in logs" :key="index">
{{ item }}
</li>
</ul>
<p v-if="lastPoint">Last point: ({{ lastPoint.x }}, {{ lastPoint.y }})</p>
</div>
</div>
</template><script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { DragAndDropPoint, DnDManagerEvents } from '@havue/drag-and-drop'
import { DnDManagerInstance } from '@havue/drag-and-drop'
import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/components'
const status = ref('idle')
const lastPoint = ref<DragAndDropPoint | null>(null)
const logs = ref<string[]>([])
const addLog = (text: string) => {
logs.value.unshift(text)
}
const clearLogs = () => {
logs.value = []
status.value = 'idle'
lastPoint.value = null
}
const handleDown: DnDManagerEvents['down'] = (point) => {
status.value = 'down'
lastPoint.value = point
addLog(`down: (${point.x}, ${point.y})`)
}
const handleFirstMove: DnDManagerEvents['first-move'] = (point) => {
status.value = 'first-move'
lastPoint.value = point
addLog(`first-move: (${point.x}, ${point.y})`)
}
const handleStart: DnDManagerEvents['start'] = (point) => {
status.value = 'start'
lastPoint.value = point
addLog(`start: (${point.x}, ${point.y})`)
}
const handleMove: DnDManagerEvents['move'] = ({ type, point }) => {
status.value = `move (${String(type)})`
lastPoint.value = point
addLog(`move: type=${String(type)}, point=(${point.x}, ${point.y})`)
}
const handleEnd: DnDManagerEvents['end'] = ({ type, point }) => {
status.value = `end (${String(type)})`
lastPoint.value = point
addLog(`end: type=${String(type)}, point=(${point.x}, ${point.y})`)
}
onMounted(() => {
DnDManagerInstance.on('down', handleDown)
DnDManagerInstance.on('first-move', handleFirstMove)
DnDManagerInstance.on('start', handleStart)
DnDManagerInstance.on('move', handleMove)
DnDManagerInstance.on('end', handleEnd)
})
onBeforeUnmount(() => {
DnDManagerInstance.off('down', handleDown)
DnDManagerInstance.off('first-move', handleFirstMove)
DnDManagerInstance.off('start', handleStart)
DnDManagerInstance.off('move', handleMove)
DnDManagerInstance.off('end', handleEnd)
})
</script><style scoped lang="scss">
.dnd-manager-demo {
display: flex;
gap: 16px;
padding: 12px;
background-color: rgb(20 177 177 / 20%);
.left-box {
flex: 1;
.drag-source {
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 60px;
margin-bottom: 16px;
color: #fff;
user-select: none;
background-color: #333;
}
.drop-area {
display: flex;
align-items: center;
justify-content: center;
width: 260px;
height: 160px;
color: #fff;
background-color: rgb(11 104 28);
}
}
.right-box {
flex: 1;
.status {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 8px 0;
font-weight: 600;
}
.clear-btn {
padding: 2px 8px;
font-size: 12px;
color: #333;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.log-list {
max-height: 200px;
padding-left: 16px;
overflow: auto;
font-size: 12px;
}
}
}
</style>