Drag Image
Adds a custom drag image that follows the mouse cursor while dragging.
Demo
vue
<script lang="ts" setup>
import { createDragDrop } from 'dnd-ts'
import { fromHTML, reorderItems } from 'dnd-ts/utils'
import { onMounted, onUnmounted, ref } from 'vue'
import data from './data/mock-data-persons.json'
const items = ref<{ name: string, avatar: string, id: string }[]>(data)
const container = ref<HTMLElement | null>(null)
const checked = ref<Record<string, boolean>>({})
onMounted(() => {
const instance = createDragDrop(container.value!, {
getSelectedElements: () =>
Array.from(container.value!.querySelectorAll(`[data-selected="true"]`)),
addClasses: true,
indicator: { offset: 2 },
autoScroll: true,
dragImage: {
minElements: 0,
createElement: (selectedElements: HTMLElement[]) => {
const item = items.value[Number.parseInt(selectedElements[0].getAttribute('data-index')!)]
return fromHTML(
`<div class='custom-drag-image' data-num='${selectedElements.length}'>
<img class="avatar" src="${item.avatar}" />
<span>${item.name}</span>
</div>`,
)
},
},
onDragEnd({ dragElements, dropElement, position }) {
if (!dropElement)
return
const index = Number.parseInt(dropElement.getAttribute('data-index')!)
const selectedItems = dragElements.map(
e => items.value.find(item => item.id === e.getAttribute('data-id'))!,
)
if (position === 'after') {
items.value = reorderItems(items.value, selectedItems, index + 1)
}
else if (position === 'before') {
items.value = reorderItems(items.value, selectedItems, index)
}
},
})
onUnmounted(() => instance.destroy())
})
</script>
<template>
<div
ref="container"
class="demo"
style="overflow: auto; max-height: 400px; padding: 10px"
>
<ul class="list">
<li
v-for="(item, index) in items"
:key="item.id"
:data-id="item.id"
:data-index="index"
class="list-item"
style="margin-bottom: 4px"
:data-selected="checked[item.id]"
@click="checked[item.id] = !checked[item.id]"
>
<img src="/handle.svg">
<input v-model="checked[item.id]" type="checkbox">
<img class="avatar" :src="item.avatar">
<span>{{ item.name }}</span>
</li>
</ul>
</div>
</template>
<style type="text/css">
[data-selected="true"] {
background: #eee !important;
}
.avatar {
border-radius: 30px;
overflow: hidden;
width: 20px;
}
.custom-drag-image:not([data-num="1"]):after {
content: attr(data-num);
position: absolute;
display: block;
background: black;
border-radius: 30px;
color: #fff;
min-width: 20px;
line-height: 1;
padding: 4px 3px;
text-align: center;
top: -10px;
font-size: 12px;
right: 10px;
}
.custom-drag-image {
position: relative;
background: #fff;
padding: 6px 10px;
margin: 10px;
display: flex;
box-shadow: 3px 3px 38px -15px rgba(0, 0, 0, 0.75);
gap: 6px;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
border-radius: 6px;
}
.custom-drag-image span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 120px;
}
</style>Types
ts
export interface DragImagePluginOptions {
/**
* Method that creates the drag image element.
*
* Defaults to: defaultCreateElementFn
*/
createElement: (selectedElements: HTMLElement[]) => HTMLElement
/**
* Minium amount of elements required to use custom drag image instead of
* browser default.
*
* Defaults to: 0
*/
minElements: number
}ts
import type { DragDropPlugin } from '../types'
import type { DragImagePluginOptions } from './types'
import { fromHTML } from '../utils'
function isTouchEvent(e: Event): e is TouchEvent {
return typeof TouchEvent !== 'undefined' && e instanceof TouchEvent
}
export function defaultCreateElementFn(selectedElements: HTMLElement[]) {
return fromHTML(
`<div class='drag-image'>${selectedElements.length} Element(s)</div>`,
)
}
export function updateContainerStyle(element: HTMLElement, top: number, left: number) {
element.setAttribute(
'style',
`position: fixed; z-index: 9999; top: ${top}px; left:${left}px; pointer-events: none;`,
)
}
export const DEFAULTS: DragImagePluginOptions = {
createElement: defaultCreateElementFn,
minElements: 0,
}
function dragImage(options?: Partial<DragImagePluginOptions>): DragDropPlugin {
const { createElement, minElements } = options
? { ...DEFAULTS, ...options }
: DEFAULTS
let removeListeners: (() => void) | null = null
const customImageContainer = document.createElement('div')
const blankDragImage = document.createElement('canvas')
blankDragImage.width = blankDragImage.height = 1
const stop = () => {
removeListeners?.()
removeListeners = null
customImageContainer.remove()
blankDragImage.remove()
}
const startDesktop = (originalEvent: DragEvent) => {
document.body.appendChild(blankDragImage)
originalEvent.dataTransfer?.setDragImage(blankDragImage, 0, 0)
const onMove = (e: DragEvent) =>
updateContainerStyle(customImageContainer, e.clientY, e.clientX)
document.addEventListener('dragover', onMove)
removeListeners = () => document.removeEventListener('dragover', onMove)
updateContainerStyle(customImageContainer, originalEvent.clientY, originalEvent.clientX)
}
const startTouch = (originalEvent: TouchEvent) => {
const t = originalEvent.touches[0]
if (t)
updateContainerStyle(customImageContainer, t.clientY, t.clientX)
const onMove = (e: TouchEvent) => {
const touch = e.touches[0]
if (touch)
updateContainerStyle(customImageContainer, touch.clientY, touch.clientX)
}
document.addEventListener('touchmove', onMove)
removeListeners = () => document.removeEventListener('touchmove', onMove)
}
return {
DragStart({ originalEvent, dragElements }) {
if (dragElements.length <= minElements)
return
customImageContainer.innerHTML = ''
customImageContainer.appendChild(createElement(dragElements))
document.body.appendChild(customImageContainer)
if (isTouchEvent(originalEvent))
startTouch(originalEvent)
else startDesktop(originalEvent as DragEvent)
},
DragEnd: stop,
}
}
export { dragImage }
export default dragImage