Draggable List
Reorder virtualized items using native drag and drop
Reorder items using native drag and drop. Virtualization maintains performance even during complex list mutations.
A
A Item 0
ID: 0
B
B Item 1
ID: 1
C
C Item 2
ID: 2
D
D Item 3
ID: 3
E
E Item 4
ID: 4
<script setup lang="ts">
import type { Ref } from 'vue';
import { VirtualScroll } from '@pdanpdan/virtual-scroll';
import { inject, ref } from 'vue';
import ExampleContainer from '#/components/ExampleContainer.vue';
import ScrollStatus from '#/components/ScrollStatus.vue';
import { useExampleScroll } from '#/lib/useExampleScroll';
import { html as highlightedCode } from './+Page.vue?highlight';
interface DraggableItem {
id: number;
label: string;
color: string;
}
const items = ref<DraggableItem[]>(
Array.from({ length: 1000 }, (_, i) => ({
id: i,
label: `${ String.fromCharCode(65 + i % 26) } Item ${ i }`,
color: `hsl(${ (i * 137.5) % 360 }, 70%, 60%)`,
})),
);
const debugMode = inject<Ref<boolean>>('debugMode', ref(false));
const draggedIndex = ref<number | null>(null);
const dropTargetIndex = ref<number | null>(null);
const {
virtualScrollRef,
scrollDetails,
onScroll,
} = useExampleScroll();
let scrollInterval: ReturnType<typeof setInterval> | null = null;
function stopAutoScroll() {
if (scrollInterval !== null) {
clearInterval(scrollInterval);
scrollInterval = null;
}
}
function startAutoScroll(direction: 'up' | 'down') {
if (scrollInterval !== null) {
return;
}
scrollInterval = setInterval(() => {
if (!virtualScrollRef.value) {
return;
}
const { scrollOffset } = virtualScrollRef.value.scrollDetails;
const delta = direction === 'up' ? -10 : 10;
virtualScrollRef.value.scrollToOffset(null, scrollOffset.y + delta, { behavior: 'auto' });
}, 16);
}
/**
* Handles the start of a drag operation.
*
* @param index - The index of the item being dragged.
* @param event - The native drag event.
*/
function handleDragStart(index: number, event: DragEvent) {
draggedIndex.value = index;
if (event.dataTransfer) {
const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
const clientX = (event as unknown as TouchEvent).touches ? (event as unknown as TouchEvent).touches[ 0 ].clientX : event.clientX;
const clientY = (event as unknown as TouchEvent).touches ? (event as unknown as TouchEvent).touches[ 0 ].clientY : event.clientY;
const x = clientX - rect.left;
const y = clientY - rect.top;
if (event.dataTransfer.setDragImage) {
event.dataTransfer.setDragImage(target, x, y);
}
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', index.toString());
}
}
/**
* Handles an item being dragged over another item.
*
* @param index - The index of the item being dragged over.
*/
function handleDragOver(index: number, event: DragEvent) {
dropTargetIndex.value = index;
// Auto-scroll logic
const container = (event.currentTarget as HTMLElement).closest('.virtual-scroll-container');
if (container) {
const rect = container.getBoundingClientRect();
const threshold = 60;
if (event.clientY < rect.top + threshold) {
startAutoScroll('up');
} else if (event.clientY > rect.bottom - threshold) {
startAutoScroll('down');
} else {
stopAutoScroll();
}
}
}
/**
* Handles the drop event to reorder the list.
*/
function handleDrop() {
stopAutoScroll();
if (draggedIndex.value !== null && dropTargetIndex.value !== null) {
const list = [ ...items.value ];
const [ draggedItem ] = list.splice(draggedIndex.value, 1);
list.splice(dropTargetIndex.value, 0, draggedItem);
items.value = list;
}
draggedIndex.value = null;
dropTargetIndex.value = null;
}
/**
* Handles the drag end event to clean up.
*/
function handleDragEnd() {
draggedIndex.value = null;
dropTargetIndex.value = null;
stopAutoScroll();
}
</script>
<template>
<ExampleContainer :code="highlightedCode">
<template #title>
<span class="example-title example-title--group-5">Draggable List</span>
</template>
<template #description>
Reorder items using native drag and drop. Virtualization maintains performance even during complex list mutations.
</template>
<template #icon>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="example-icon example-icon--group-5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</template>
<template #controls>
<ScrollStatus :scroll-details="scrollDetails" />
</template>
<template #subtitle>
Reorder virtualized items using native drag and drop
</template>
<VirtualScroll
ref="virtualScrollRef"
class="example-container"
:items="items"
:debug="debugMode"
aria-label="Reorderable list"
@scroll="onScroll"
>
<template #item="{ item, index }">
<div
role="button"
tabindex="0"
class="example-vertical-item py-2 outline-none bg-base-100 focus-visible:bg-base-300"
:class="{
'opacity-30': draggedIndex === index,
'border-t-4 border-t-primary': dropTargetIndex === index && draggedIndex !== index,
}"
@dragstart="handleDragStart(index, $event)"
@dragover.prevent="handleDragOver(index, $event)"
@drop="handleDrop"
@dragend="handleDragEnd"
@keydown.enter.prevent
@keydown.space.prevent
>
<div
class="size-10 rounded-lg me-4 flex items-center justify-center text-white font-bold shadow-sm"
:style="{ backgroundColor: item.color }"
>
{{ item.label[0] }}
</div>
<div>
<div class="font-bold text-sm">{{ item.label }}</div>
<div class="text-xs opacity-40 font-mono">ID: {{ item.id }}</div>
</div>
<div
class="ms-auto p-2 cursor-grab active:cursor-grabbing opacity-30 hover:opacity-100 touch-pan-y select-none"
draggable="true"
:aria-label="`Drag handle for ${ item.label }`"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</div>
</div>
</template>
</VirtualScroll>
</ExampleContainer>
</template>
- Scroll Status
- Directionvertical
- Current Item #-
- Rendered Range #0:0
- Total Size (px)0w ×0h
- Viewport Size (px)0w ×0h
- Scroll Offset (px)0x ×0y