Documentación/Guías
Multi-drag y selección
Selecciona varios ítems con clics o con un rectángulo de selección y arrástralos juntos — como un bloque, dejando huecos o apilados en el overlay —, acepta solo algunos al soltar y muéstralos en un overlay propio.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
Selecciona unos cuantos archivos y arrastra uno de ellos: los demás lo acompañan. Cambia
multiple para ver qué hacen por el camino, y la disposición para ver el overlay de una
cuadrícula.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Files from './Files.vue';
</script>
<template>
<DnDProvider>
<Files />
</DnDProvider>
</template>La selección
Cada provider guarda una selección: un conjunto reactivo de ids. Arrastrar un ítem seleccionado arrastra todos los ítems seleccionados, empezando por el que se agarró. Arrastrar un ítem que no está seleccionado arrastra solo ese ítem.
La biblioteca no decide cómo se selecciona: las reglas de los clics son tuyas. Tampoco vacía nunca la selección por su cuenta, y la selección sobrevive a los desmontajes, como los de las filas que recicla una lista virtual.
Clics: Ctrl/Cmd y Mayús
Cada ítem ordenable y cada elemento arrastrable devuelve selected, una ref modificable ligada a
la selección. Vincúlala a una clase, a una casilla (v-model="selected") o cámbiala en un
manejador de clic:
<script setup lang="ts">
const { selected } = makeSortableItem(el, { item: () => props.file });
</script>
<template>
<li ref="el" class="file" :class="{ selected }" @click="emit('pick', $event)">…</li>
</template>
La lista de la demo pone las reglas: un clic selecciona un archivo, Ctrl/Cmd-clic añade o quita uno, Mayús-clic selecciona el tramo desde el último archivo pulsado:
const { selection } = useDnD();
let anchor: string | null = null;
const pick = (id: string, event: MouseEvent) => {
const ids = items.value.map((file) => file.id);
if (event.shiftKey && anchor) {
const [a, b] = [ids.indexOf(anchor), ids.indexOf(id)];
selection.set(ids.slice(Math.min(a, b), Math.max(a, b) + 1));
return;
}
if (event.ctrlKey || event.metaKey) selection.toggle(id);
else selection.set([id]);
anchor = id;
};
El clic que termina un arrastre no llega a @click, así que un drop nunca cambia la selección
por accidente. Usa @click, no pointerup, para las acciones de los ítems.
Desde cualquier sitio: useDnD().selection
import { useDnD, useDnDMonitor } from '@vue-dnd-kit/core';
const { selection } = useDnD();
selection.has('a'); // is it selected?
selection.add('a', 'b'); // select
selection.delete('a'); // deselect
selection.toggle('a'); // flip it, or force it: toggle('a', true)
selection.set(['a', 'c']); // exactly these
selection.clear();
selection.ids; // a reactive ReadonlySet<Key>
// Clear it after every drop, if your UI wants that:
useDnDMonitor({ onDragEnd: () => selection.clear() });
La selección se indexa por id, así que puede contener ítems de varias listas y también elementos arrastrables simples: un arrastre se lleva todos los ítems seleccionados bajo el provider.
Rectángulo de selección
makeSelectionArea convierte un elemento en un área donde una pulsación en el espacio vacío
dibuja un rectángulo, y los ítems que cruza quedan seleccionados. En este escritorio, dibuja un
rectángulo alrededor de unos iconos y arrastra uno de ellos:
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Desktop from './Desktop.vue';
</script>
<template>
<!-- Every selected icon gets a clone at pickup: the overlay shows them all. -->
<DnDProvider overlay-previews="all">
<Desktop />
</DnDProvider>
</template><script setup lang="ts">
const desktop = useTemplateRef<HTMLElement>('desktop');
const { isSelecting, style } = makeSelectionArea(desktop);
</script>
<template>
<!-- The area needs a position: the band is placed inside it. -->
<div ref="desktop" class="desktop">
<DesktopIcon v-for="icon in icons" :key="icon.id" :icon="icon" />
<div v-if="isSelecting" class="band" :style="style" />
</div>
</template>
<style scoped>
.desktop { position: relative; height: 236px; }
.band { border: 1px solid #42b883; background: color-mix(in srgb, #42b883 15%, transparent); }
</style>
- El rectángulo lo renderizas tú:
stylelo coloca, relativo al contenido del área. - El área necesita una posición no estática (
relativesirve), o el rectángulo acaba en otro sitio. - Un rectángulo solo empieza desde la propia área, así que hazla grande: todo el panel, con espacio alrededor de los ítems, no un envoltorio ajustado.
- Una pulsación sobre un ítem lo arrastra. Los botones, los campos y la barra de desplazamiento del área conservan sus pulsaciones.
- El rectángulo sigue anclado al contenido mientras el área o la página se desplazan. Escape lo cancela y restaura la selección.
useDnD().statuses'selecting'mientras se dibuja un rectángulo: la línea de debajo del escritorio lo lee.
| Opción | Qué hace |
|---|---|
strategy | 'toggle' (por defecto): los ítems que cruza el rectángulo cambian de estado. 'select': solo los ítems dentro del rectángulo acaban seleccionados, así que un clic en el espacio vacío deselecciona los ítems del área. |
modifier | Teclas que hay que mantener para dibujar: { keys: ['Shift'], method: 'every' } (o 'some'). |
accept | Qué elementos arrastrables selecciona el rectángulo, una regla de aceptación comparada con su type. |
disabled | Sin rectángulo; activarlo cancela el que esté en curso. |
onSelected(ids) | Se llama cuando termina un rectángulo, con la selección. |
strategy, modifier y disabled aceptan un valor, una ref o un getter, que se leen cuando una | |
| pulsación empieza un rectángulo. Con áreas dentro de áreas, la más interior se queda la | |
| pulsación. La página de la API de makeSelectionArea las cambia en | |
| vivo. |
Moverlos tal como están
Los iconos son elementos arrastrables simples (makeDraggable), cada uno con su icono como
data. Su overlay es un componente propio de la demo: coloca un DragPreview de cada icono
arrastrado tal como están los iconos en el escritorio, alrededor del agarrado. Los iconos esperan
en su sitio, atenuados por isDragging, que sigue siendo true hasta que sus copias han
aterrizado.
<script setup lang="ts">
const props = defineProps<{ items: DragItem[] }>();
const placed = computed(() => {
const grabbed = props.items[0]!.data as Icon;
return props.items.map(({ id, data }) => {
const icon = data as Icon;
return { id, left: `${icon.x - grabbed.x}px`, top: `${icon.y - grabbed.y}px` };
});
});
</script>
<template>
<div class="formation">
<DragPreview v-for="{ id, left, top } in placed" :id="id" :key="id" class="copy" :style="{ left, top }" />
</div>
</template>
const { isDragging, selected } = makeDraggable(el, { id, data: () => props.icon, overlay: Formation });
El escritorio recibe el drop. Su position es donde se soltó el icono agarrado, en las
coordenadas del escritorio: el icono va ahí, y los demás se mueven lo mismo. Luego cada copia
aterriza sobre su propio icono en su nuevo sitio. Si se sueltan fuera del escritorio, las copias
vuelven a los iconos.
makeDroppable(desktop, {
id: 'desktop',
onDrop: ({ active, position }) => {
const grabbed = icons.value.find((icon) => icon.id === active.id)!;
const shift = { x: position!.x - grabbed.x, y: position!.y - grabbed.y };
for (const icon of icons.value) {
if (!active.ids.includes(icon.id)) continue;
icon.x += shift.x; // the demo also keeps each icon on the desktop
icon.y += shift.y;
}
},
});
Por defecto, al agarrar se toma un clon de 5 ítems como máximo: overlay-previews="all" en el
provider toma uno para cada icono seleccionado.
Dentro de una carpeta
En esta ventana, el rectángulo selecciona archivos, y las carpetas los aceptan. El overlay es el de por defecto: un montón con los archivos elegidos. Al soltarlos en una carpeta, entran uno tras otro, el agarrado el último, y la carpeta se los traga. Si se cancela, cada uno vuelve volando a su archivo. Haz clic en una carpeta para abrirla, y arrastra archivos de vuelta a Home en la ruta.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Finder from './Finder.vue';
</script>
<template>
<!-- Every picked file gets a clone at pickup: they all go into the folder. -->
<DnDProvider overlay-previews="all">
<Finder />
</DnDProvider>
</template><!-- Every picked file gets a clone at pickup: they all go in. -->
<DnDProvider overlay-previews="all">
<Finder />
</DnDProvider>
makeDroppable(el, {
id: () => props.folder.id,
accept: 'file',
onDrop: (event) => emit('move', event.active.ids),
});
// The files are in: the folder gulps.
useDnDMonitor({
onDragSettle: (event) => {
if ('dropped' in event && event.over?.id === props.folder.id) gulps.value++;
},
});
Eso es todo: los archivos salen de la ventana con el drop, así que no tienen otro sitio donde
aterrizar que la carpeta, y la animación al soltar por defecto mete un multi-drag en una zona
un ítem tras otro. overlayPreviews dice cuántos ítems reciben un clon al agarrar (5 por
defecto). Todos los clones vuelan: a la zona, a su hueco o de vuelta a su ítem.
La carpeta es un botón: un rectángulo nunca empieza sobre botones ni campos, así que un clic la abre.
Cómo se mueven los demás ítems: multiple
La lista donde empieza un multi-drag decide qué hacen los demás ítems seleccionados mientras dura. Cámbialo en la primera demo de esta página:
makeSortableList(listRef, { items: files, multiple: 'gaps' });
multiple | Durante el arrastre | El overlay |
|---|---|---|
'group' (por defecto) | Los demás ítems dejan su sitio y viajan con el agarrado como un solo bloque de huecos, así que la lista ya muestra el resultado del drop. | Una columna de los ítems arrastrados, en el sentido de la lista (hasta overlayPreviews). |
'gaps' | Los demás se quedan donde están, marcados con data-dnd-dragging, hasta el drop. | La misma columna. |
'stack' | Los demás vuelan al overlay. | El ítem agarrado con un contador. |
Al soltar, los ítems aterrizan juntos en el índice de destino, en el orden que tenían. En
'group' y 'gaps', cada ítem del overlay vuela a su propio hueco. La opción se lee cuando
empieza un arrastre, así que una ref o un getter la cambia.
Sobre un destino de drop que no es una lista, como una papelera o una playlist, o sobre una lista
que rechaza el arrastre, un bloque 'group' se deshace: cada ítem vuelve a mostrarse en su propio
sitio, tal como lo dejaría un drop ahí, y el bloque se vuelve a formar en cuanto se pasa por
encima de una lista.
Da estilo a los ítems que se quedan atrás en 'gaps', atenuados por defecto:
.file[data-dnd-dragging] {
opacity: 0.35;
}
El overlay por defecto
En 'group' y 'gaps', el overlay es una columna de los ítems arrastrados tal como aterrizarán:
el agarrado bajo el puntero y los demás antes y después de él en el orden del drop (una fila en
las listas horizontales). Muestra tantos como indique el overlayPreviews del provider (5 por
defecto, 'all' para todos los renderizados). Los que pasan de ahí se cuentan donde están: +N
al principio de la columna para los ítems que aterrizan antes de los mostrados, +N al final
para los de después.
En las cuadrículas, una columna de celdas enteras taparía la página, así que el overlay es un montón: hasta tres ítems abiertos en abanico detrás del agarrado, con el total encima. Cambia la demo a Grid para verlo.
Los elementos arrastrables simples (makeDraggable) también forman un montón, como hacen los
archivos de arriba. Cada clon del overlay aterriza por su cuenta: en su hueco tras un drop en una
lista, sobre su ítem dondequiera que lo haya dejado el drop (de vuelta en su sitio tras una
cancelación), y dentro de la zona en la que entra un drop cuando los ítems ya no están en la
página, uno tras otro, la caja la última.
Aceptar solo algunos
El onBeforeDrop de una lista puede aceptar parte de un multi-drag: devuelve las claves que
acepta, y los demás vuelven de donde vinieron. Puede ser asíncrono y preguntar: el overlay espera
donde se soltó hasta que llega la respuesta. Aquí Scheduled quiere publicaciones con portada. Si
se suelta con una que no la tiene, pregunta si añadir las portadas que faltan y aceptarlas todas,
aceptar solo las publicaciones listas, o ninguna:
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Blog from './Blog.vue';
</script>
<template>
<DnDProvider>
<Blog />
</DnDProvider>
</template>const { items } = makeSortableList(list, {
id: props.id,
items: posts,
group: 'blog',
onBeforeDrop: async (event) => {
const missing = event.items.filter((post) => post.cover === null);
if (!props.ask || event.sameList || !missing.length) return true;
const choice = await props.ask(missing, event.items.length - missing.length); // the question under the lists
if (choice === 'cover') missing.forEach((post) => (post.cover = pickCover()));
if (choice === 'ready') return event.keys.filter((_, index) => event.items[index]!.cover !== null);
return choice === 'cover'; // all of them, or none: they fly back
},
});
event.keysson las claves arrastradas, yevent.items[i]es el ítem deevent.keys[i].- Tras la respuesta,
onDroprecibeacceptedyrejected, yevent.resultcontiene las listas tal como las deja el drop parcial. - Mientras el drop espera, las listas siguen mostrando cada publicación arrastrada donde
aterrizaría: la respuesta decide. Para una regla que no necesita pregunta,
group.putdecide mientras pasas por encima (consulta tipos mezclados más abajo). Más en decidir un drop.
Un límite
En este tablero, In progress y Review tienen un límite de tarjetas. Una columna llena no acepta
nada: su group.put lo rechaza, y el montón se vuelve rojo sobre ella. Una columna con espacio
para algunas de las tarjetas elegidas acepta tantas como quepan, y las demás vuelven a su sitio.
El overlay de la columna lo dice antes del drop.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Board from './Board.vue';
</script>
<template>
<DnDProvider>
<Board />
</DnDProvider>
</template>const room = (keys: Key[]) => limit - column.cards.filter((card) => !keys.includes(card.id)).length;
makeSortableList(list, {
items: cards,
group: { name: 'board', put: ({ keys, from, to }) => from === to || room(keys) > 0 },
multiple: 'stack',
overlay: CardPile, // red where `accepted` is false
overOverlay: fit,
onBeforeDrop: (event) => {
if (event.sameList) return true;
const free = room(event.keys);
return free >= event.keys.length || event.keys.slice(0, free);
},
});
fit es una función, que Vue toma como un componente funcional. Le pasa a FitBadge lo que
solo sabe la columna, su espacio libre, y le pasa el montón como slot del badge.
const fit: FunctionalComponent<OverlayProps> = (overlay, { slots }) =>
h(FitBadge, { count: overlay.items.length, room: room(overlay.items.map((item) => item.id)), limit }, slots);
Tipos mezclados
Una lista también puede rechazar algunos ítems por su tipo, antes del drop. Con
acceptMode: 'some', se pregunta a group.put por cada ítem por separado: la lista acepta el
arrastre cuando encaja al menos un ítem, y el resto vuelve a su sitio al soltar. Con 'all' (el
valor por defecto), todos los ítems deben encajar.
makeSortableList(gallery, {
items: photos,
group: { name: 'files', put: ({ types }) => types.includes('image') },
acceptMode: 'some',
});
Los destinos de drop simples también aceptan acceptMode. Consulta
listas que comparten ítems y
arrastrar y soltar.
Un overlay propio para varios ítems
Los componentes de overlay y el slot #overlay del provider reciben items: todos los ítems
arrastrados, empezando por el agarrado. Muestra un resumen en lugar de los propios ítems: aquí un
contador, el primer asunto y lo que hará el drop.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>
<template>
<DnDProvider>
<Mailbox />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null }>();
const first = computed(() => props.items[0]?.item as Mail);
const count = computed(() => props.items.length);
// What the drop does, from the `data` of the zone under it.
const zone = computed(() => props.over?.data as { verb: string; danger?: boolean } | undefined);
const action = computed(() => {
const noun = count.value === 1 ? 'conversation' : 'conversations';
return `${zone.value?.verb ?? 'Move'} ${count.value} ${noun}`;
});
</script>
<template>
<!-- The box keeps the row's size; the summary hangs off its middle, by the pointer. -->
<div class="box">
<div class="summary" :class="{ stack: count > 1, danger: zone?.danger }">
<span class="count">{{ count }}</span>
<strong>{{ action }}</strong>
<small>{{ first.subject }}{{ count > 1 ? ` and ${count - 1} more` : '' }}</small>
</div>
</div>
</template>
<style scoped>
.box { position: relative; width: 100%; height: 100%; }
.summary { position: absolute; top: calc(50% + 10px); left: calc(50% + 10px); width: 230px; }
</style>
// The box is centered on the pointer, so the summary hangs off the pointer and leaves
// the zone under it in sight.
const { items } = makeSortableList(list, { items: mails, overlay: MailOverlay, modifiers: [snapCenterToCursor] });
// A zone takes the mails out of the inbox; its `data` tells the overlay what it does.
makeDroppable(archive, {
id: 'Archive',
data: { verb: 'Archive' },
onDrop: (event) => {
event.source?.remove();
selection.clear();
},
});
Para disponer los ítems reales en su lugar, renderiza sus clones con DragPreview, como hace el
escritorio de arriba; la
guía del overlay abre en abanico un
montón de libros. Los clones se toman al agarrar, para el ítem agarrado y los demás junto a él
en el orden del drop, tantos como indique overlayPreviews; para cualquier otro ítem,
DragPreview renderiza su slot por defecto. Al soltar, cada vista previa aterriza sobre su propio
ítem.
Árboles
En un árbol, los nodos seleccionados se mueven como un solo bloque, cada uno con su subárbol, en el orden del documento. Un nodo seleccionado dentro de otro seleccionado viaja dentro del subárbol de su padre. Consulta árboles.