Documentation/Guides

Décider d'un dépôt

Accepter, refuser ou différer un dépôt — des règles pendant le glissement ou au relâchement, une partie d'un multi-drag, une boîte de dialogue, un serveur qui doit d'abord être d'accord, des moniteurs qui opposent leur veto, et les dépôts en dehors de toute cible.

Modifier cette page

Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais

Faites glisser une carte dans Done. Le dépôt attend votre réponse : déplacer la carte, continuer à la faire glisser ailleurs, ou annuler.

Live

Chargement de la démo…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Board from './Board.vue';
</script>

<template>
  <DnDProvider>
    <Board />
  </DnDProvider>
</template>

onBeforeDrop

makeSortableList(listRef, {
  items: tasks,
  onBeforeDrop: (e) => (e.items.some((task) => task.locked) ? 'reject' : 'accept'),
});

Les listes, les arbres, les cibles de dépôt (makeDroppable) et les moniteurs (useDnDMonitor) acceptent un onBeforeDrop. Il s'exécute quand le glissement est relâché, avant que quoi que ce soit ne change, et sa réponse décide :

RéponseCe qui se passe
'accept', true ou rienLe dépôt aboutit.
'reject' ou falsePas de dépôt : les items reviennent à leur place. Une erreur levée refuse aussi.
Un tableau de clésSeuls ces items sont déposés ; les autres reviennent à leur place.
'continue'Pas de dépôt, et le glissement continue.
Une promesse de l'une de ces réponsesL'overlay attend là où il a été relâché jusqu'à ce que la promesse soit réglée.

Les listes et les arbres passent un SortableDropEvent : keys, items, from, to, mode, sameList et result, les nouveaux tableaux de chaque liste que le dépôt modifie. Les cibles de dépôt et les moniteurs reçoivent le DnDEvent générique : active, over, position et source. Voir les événements.

Une règle pendant le glissement, ou au relâchement

Email et Chat acceptent tous deux des pièces jointes de 10 Mo au plus. Faites glisser demo.mov au-dessus de chacun : Email la refuse pendant le survol, Chat l'accepte et la renvoie quand vous relâchez.

Live

Chargement de la démo…

<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';

const columns = reactive([
  {
    id: 'desktop',
    title: 'Desktop',
    files: [
      { id: 'notes', name: 'notes.pdf', size: 2 },
      { id: 'demo', name: 'demo.mov', size: 48 },
      { id: 'logo', name: 'logo.png', size: 1 },
      { id: 'backup', name: 'backup.zip', size: 120 },
    ],
  },
  // Both take files of 10 MB at most, and say so at different times.
  { id: 'email', title: 'Email', rule: 'put' as const, files: [] },
  { id: 'chat', title: 'Chat', rule: 'onBeforeDrop' as const, files: [] },
]);
</script>

<template>
  <DnDProvider>
    <div class="board">
      <Column v-for="column in columns" :key="column.id" :column="column" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.board {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}

.board > * {
  flex: 0 1 170px;
  min-width: 150px;
}
</style>
Column.vue
// Asked while dragging: a refused drag never enters the list.
group: { name: 'attachments', put: ({ items }) => fits(items as Attachment[]) },

// Asked on release: the drag enters the list, then flies back.
onBeforeDrop: (e) => (fits(e.items) ? 'accept' : 'reject'),
group.put (ou l'accept d'une cible)onBeforeDrop
InterrogéPendant le glissement, dès que le glissement atteint la liste.Une fois, quand le glissement est relâché.
L'utilisateur voitLa liste refuse tout de suite : aucun emplacement ne s'ouvre, isAccepting vaut false, over.accepted vaut false.L'emplacement s'ouvre comme d'habitude ; les items reviennent à leur place au relâchement.
ConnaîtCe qui est glissé : keys, items, types, from, to.Tout le dépôt : to.index, sameList, mode, result.
PeutDire oui ou non.Attendre une promesse, accepter certaines clés, prolonger le glissement.

Utilisez group.put pour ce qui se sait à partir des items seuls (un type, un indicateur, un nombre) : l'utilisateur l'apprend avant de relâcher. Utilisez onBeforeDrop pour ce qui dépend de l'endroit, de la réponse de l'utilisateur ou de celle du serveur. Les règles propres à la cible passent en premier : au-dessus d'une liste ou d'une cible qui refuse le glissement, le dépôt est annulé et son onBeforeDrop n'est pas interrogé.

Accepter une partie des items

Certaines règles ne peuvent être vérifiées qu'au relâchement, auprès d'un serveur. Les places de cet atelier sont aussi réservées par d'autres : au relâchement, la liste demande au serveur combien il en reste, et accepte autant de personnes parmi celles qui ont été glissées. Cochez-en quelques-unes, et faites-les glisser ensemble.

Live

Chargement de la démo…

<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';

const columns = reactive([
  {
    id: 'waiting',
    title: 'Waiting list',
    people: [
      { id: 'ada', name: 'Ada', hue: 200 },
      { id: 'bo', name: 'Bo', hue: 30 },
      { id: 'cy', name: 'Cyrus', hue: 280 },
      { id: 'dee', name: 'Dee', hue: 140 },
    ],
  },
  {
    id: 'workshop',
    title: 'Workshop',
    seats: true,
    people: [
      { id: 'eli', name: 'Eli', hue: 340 },
      { id: 'fay', name: 'Fay', hue: 90 },
    ],
  },
]);
</script>

<template>
  <DnDProvider>
    <div class="demo">
      <p class="tip">Tick a few people, then drag one of them into the workshop</p>
      <div class="board">
        <Column v-for="column in columns" :key="column.id" :column="column" />
      </div>
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

.tip {
  margin: 0;
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.55;
}

.board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 230px));
  justify-content: center;
  gap: 14px;
  width: 100%;
}
</style>
Column.vue
makeSortableList(list, {
  items: toRef(props.column, 'people'),
  group: 'workshop',
  // Some keys: those land, the others fly back to where they came from.
  onBeforeDrop: async (e) => {
    const { free } = await seatsLeft(props.column.people.length); // the overlay waits
    return e.keys.slice(0, free);
  },
  onDrop: (e) => {
    selection.delete(...e.accepted); // the ones sent back stay selected
    notice.value = `${e.accepted.length} booked, ${e.rejected.length} back to the waiting list`;
  },
});

Dans un multi-drag, les clés que vous renvoyez atterrissent ensemble et les autres reviennent à leur place. onDrop les voit ensuite dans e.accepted et e.rejected, et son e.result est le résultat final. Un tableau vide n'accepte rien : le dépôt est refusé.

Continuer à glisser : 'continue'

'continue' annule le dépôt, pas le glissement. Répondez Keep dragging dans la boîte de dialogue ci-dessus : l'item reste en main depuis l'endroit où il était, la page repasse en mode glissement, et l'utilisateur continue.

  • À la souris ou au stylet, l'item suit le pointeur sans bouton enfoncé. Le relâchement suivant — un clic — le dépose, et ce clic n'atteint jamais la page en dessous.
  • Au toucher, l'item est repris là où le doigt se pose ; lever le doigt le dépose.
  • Un glissement au clavier continue : les flèches déplacent l'item, Space ou Enter le déposent.
  • Escape l'annule, comme n'importe quel glissement.
  • Le dépôt suivant interroge de nouveau onBeforeDrop.

Les moniteurs entendent onDragResume, et les lecteurs d'écran annoncent que l'item n'a pas été déposé et qu'il est toujours en mouvement (remplacez le texte avec announcements.onDragResume du provider).

useDnDMonitor({
  onDragResume() {
    hint.value = 'Still dragging: click to drop, Esc to cancel';
  },
});

Demander à l'utilisateur

onBeforeDrop peut renvoyer une promesse. Ouvrez une boîte de dialogue, et résolvez la promesse avec le bouton que choisit l'utilisateur :

Column.vue
const { items } = makeSortableList(list, {
  id: props.column.id,
  items: toRef(props.column, 'tasks'),
  group: 'release',
  onBeforeDrop: (e) => (props.column.confirm && !e.sameList ? props.ask(e) : 'accept'),
});
Board.vue
type Answer = 'accept' | 'continue' | 'reject';
const question = shallowRef<{ title: string; resolve: (answer: Answer) => void } | null>(null);

/** Opens the dialog: `onBeforeDrop` waits for the button the user picks. */
const ask = (e: SortableDropEvent<Task>) =>
  new Promise<Answer>((resolve) => {
    question.value = { title: e.items[0]!.title, resolve };
  });

Tant que la promesse est en attente :

  • l'overlay attend là où il a été relâché, marqué data-dnd-overlay="dropping" ;
  • useDnD() garde l'état qu'il avait au relâchement, et status vaut 'dropping' ;
  • la page est sortie du mode glissement : le curseur et la sélection de texte sont revenus, et une boîte de dialogue fonctionne donc normalement ;
  • aucun autre glissement ne peut commencer.
Un <dialog> modal ouvert avec showModal() se trouve dans la top layer, au-dessus de l'overlay en attente. Transformez son événement cancel (Escape) en 'reject', comme le fait l'exemple.

L'ordre des hooks

Réordonnez les notes, déposez-en une dans la corbeille, puis changez une réponse et recommencez. Le panneau liste les hooks du dernier dépôt dans l'ordre où ils se sont exécutés.

Live

Chargement de la démo…

<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Monitor from './Monitor.vue';
import Notes from './Notes.vue';
import { ANSWERS, type Answer } from './types';

const monitorAnswer = ref<Answer>('accept');
const targetAnswer = ref<Answer>('accept');

// Every hook of the last drop, in the order they ran.
const steps = ref<{ who: string; what: string }[]>([]);
const trace = (who: string, what: string) => steps.value.push({ who, what });
</script>

<template>
  <DnDProvider
    @drag-start="steps = []"
    @drag-end="trace('provider', '@drag-end')"
    @drag-cancel="trace('provider', '@drag-cancel')"
    @drag-settle="trace('provider', '@drag-settle: landed')"
  >
    <div class="demo">
      <div class="switches">
        <fieldset>
          <legend>monitor answers</legend>
          <label v-for="value in ANSWERS" :key="value" :class="{ on: monitorAnswer === value }">
            <input v-model="monitorAnswer" type="radio" :value="value" />
            {{ value }}
          </label>
        </fieldset>
        <fieldset>
          <legend>target answers</legend>
          <label v-for="value in ANSWERS" :key="value" :class="{ on: targetAnswer === value }">
            <input v-model="targetAnswer" type="radio" :value="value" />
            {{ value }}
          </label>
        </fieldset>
      </div>
      <div class="layout">
        <Notes :answer="targetAnswer" :trace="trace" />
        <ol class="steps" aria-label="Hooks of the last drop">
          <li v-for="(step, index) in steps" :key="index">
            <span class="who">{{ step.who }}</span>{{ step.what }}
          </li>
          <li v-if="!steps.length" class="empty">Move a note, or drop one in the trash</li>
        </ol>
      </div>
      <Monitor :answer="monitorAnswer" :trace="trace" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.switches {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 16px;
}

fieldset {
  display: inline-flex;
  margin: 0;
  padding: 3px;
  border: 0;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

legend {
  float: left;
  align-self: center;
  padding: 0 8px 0 6px;
  font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.55;
}

label {
  padding: 5px 10px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.layout {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 16px;
}

.steps {
  box-sizing: border-box;
  min-width: 220px;
  min-height: 150px;
  margin: 0;
  padding: 10px 14px 10px 32px;
  border-radius: 10px;
  background: color-mix(in srgb, currentColor 5%, transparent);
  font: 11px/1.8 ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  list-style: decimal;
}

.steps li::marker {
  color: color-mix(in srgb, currentColor 45%, transparent);
}

.who {
  display: inline-block;
  min-width: 60px;
  color: #42b883;
}

.steps li.empty {
  margin-left: -18px;
  list-style: none;
  opacity: 0.45;
}
</style>
  1. L'onBeforeDrop de chaque useDnDMonitor, tour à tour. Un 'reject' ou un 'continue' met fin aux questions ; des clés restreignent le dépôt.
  2. L'onBeforeDrop de la cible : la liste ou l'arbre où vont les items, ou la cible de dépôt.
  3. L'onDrop de la cible uniquement : une liste que des items quittent pour une autre n'est pas interrogée, et son nouveau tableau se trouve dans l'e.result de la cible. Quand des items sont retirés (une corbeille, dropOutside: 'remove'), l'onDrop de leur liste s'exécute, avec un to.index de -1.
  4. L'écriture par défaut : chaque liste que le dépôt modifie reçoit son nouveau tableau.
  5. L'onDragEnd du provider (@drag-end) et des moniteurs. Puis l'overlay atterrit, et onDragSettle (@drag-settle) l'annonce.

Un dépôt refusé passe directement à onDragCancel (@drag-cancel), et les items reviennent à leur place. Un 'continue' met aussi fin aux questions, avec onDragResume : le glissement continue, et son prochain dépôt pose de nouveau les questions.

Les moniteurs décident avant la cible : un veto ne vient donc jamais après qu'une liste a déjà appliqué un dépôt de son côté. Cela fait d'un moniteur l'endroit idéal pour des règles portant sur chaque dépôt de l'application :

Monitor.vue
const { syncing } = useSync();

// Read-only while syncing: every drop under the provider flies back.
useDnDMonitor({
  onBeforeDrop: () => (syncing.value ? 'reject' : 'accept'),
});

onDrop : prévenu avant l'écriture

makeSortableList(listRef, {
  items: tasks,
  onDrop: (e) => {
    // `tasks` still holds the old order, `e.result` the new one.
    console.log(e.keys, e.from, e.to, e.result.get(e.to.list));
  },
});

L'onDrop d'une liste s'exécute une fois le dépôt accepté, de façon synchrone, juste avant que la liste ne l'écrive. Pour agir après l'écriture, utilisez le @drag-end du provider ou useDnDMonitor({ onDragEnd }). Sur une simple cible de dépôt, onDrop est l'endroit où vous modifiez vos données : la bibliothèque ne déplace rien pour elle.

Stores et serveurs : listes contrôlées

const store = useBoardStore();
const { items } = makeSortableList(listRef, {
  id: 'todo',
  group: 'board',
  items: () => store.lists.todo, // a getter: read-only
  onDrop: (e) => {
    e.preventDefault();
    for (const [listId, next] of e.result) store.setList(listId, next);
  },
});

La bibliothèque n'écrit un dépôt que dans une ref modifiable. Un getter, un computed ou une ref readonly ne sont jamais écrits : appelez e.preventDefault() et appliquez e.result vous-même — les nouveaux tableaux de chaque liste que le dépôt modifie, par id de liste. preventDefault() saute l'écriture dans toutes ces listes, car n'en écrire que certaines perdrait ou dupliquerait des items. Les builds de développement avertissent quand le dépôt d'une liste en lecture seule est laissé à la bibliothèque.

Enregistrer d'abord sur un serveur

Déplacez une carte avec le serveur flaky (une requête sur deux échoue) ou down. La carte attend avec un badge pendant que la requête s'exécute, puis atterrit, ou revient à sa place si elle a échoué.

Live

Chargement de la démo…

<script setup lang="ts">
import { DnDProvider, DragPreview } from '@vue-dnd-kit/core';
import Column from './Column.vue';
import { mode, requests, saving, type ServerMode } from './server';

const MODES: ServerMode[] = ['reliable', 'flaky', 'down'];
</script>

<template>
  <DnDProvider>
    <div class="demo">
      <fieldset class="modes">
        <legend>server</legend>
        <label v-for="value in MODES" :key="value" :class="{ on: mode === value }">
          <input v-model="mode" type="radio" :value="value" />
          {{ value }}
        </label>
      </fieldset>
      <div class="board">
        <Column id="backlog" title="Backlog" />
        <Column id="sprint" title="Sprint" />
      </div>
      <ol class="requests" aria-label="Requests">
        <li v-for="request in requests" :key="request.id" :class="`status-${request.status}`">
          {{ request.text }} <span>{{ request.status === 'pending' ? '…' : request.status }}</span>
        </li>
        <li v-if="!requests.length" class="empty">No requests yet</li>
      </ol>
    </div>

    <!-- The overlay of every drag: the card's copy, and a badge while the server answers. -->
    <template #overlay="{ active }">
      <div class="carried">
        <DragPreview :id="active.id" />
        <span v-if="saving" class="saving">Saving…</span>
      </div>
    </template>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.modes {
  display: inline-flex;
  margin: 0;
  padding: 3px;
  border: 0;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

legend {
  float: left;
  align-self: center;
  padding: 0 8px 0 6px;
  font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.55;
}

.modes label {
  padding: 5px 10px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.modes label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

.modes label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

.modes input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.board {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 220px));
  gap: 14px;
  justify-content: center;
  width: 100%;
}

.requests {
  width: min(100%, 454px);
  min-height: 60px;
  margin: 0;
  padding: 8px 12px;
  border-radius: 10px;
  background: color-mix(in srgb, currentColor 5%, transparent);
  font: 11px/1.6 ui-monospace, Menlo, Consolas, monospace;
  list-style: none;
  white-space: pre;
  overflow-x: auto;
}

.requests li + li {
  opacity: 0.55;
}

.requests .status-200 span {
  color: #42b883;
}

.requests .status-503 span {
  color: #e5484d;
}

.requests li.empty {
  color: inherit;
  opacity: 0.45;
}

.carried {
  position: relative;
  width: 100%;
  height: 100%;
}

.saving {
  position: absolute;
  top: 50%;
  right: 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  translate: 0 -50%;
  padding: 4px 8px;
  border-radius: 99px;
  background: #42b883;
  color: #fff;
  font: 600 11px/1 system-ui, sans-serif;
}

.saving::before {
  content: '';
  width: 9px;
  height: 9px;
  border: 2px solid rgb(255 255 255 / 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin {
  to {
    rotate: 1turn;
  }
}
</style>
Column.vue
const { items } = makeSortableList(list, {
  id: props.id,
  // A getter: the library never writes it. The drop is applied below, once saved.
  items: () => board[props.id]!,
  group: 'sprint',
  onBeforeDrop: async (e) => {
    e.preventDefault();
    try {
      await saveMove(e.keys, e.to);
    } catch {
      return 'reject'; // nothing applied: the card flies back
    }
    for (const [id, tasks] of e.result) board[id] = tasks; // lands in its new place
    return 'accept';
  },
});

Appliquez e.result avant que la promesse ne se résolve, et l'overlay atterrit dans le nouvel ordre. Quand la requête échoue, n'appliquez rien et refusez : les items reviennent à leur place, et les moniteurs entendent onDragCancel.

Le badge vient du slot #overlay du provider, qui rend l'overlay de chaque glissement. Un DragPreview affiche la copie habituelle de la carte, et le badge se place par-dessus pendant que la requête s'exécute :

index.vue
<DnDProvider>
  <Board />
  <template #overlay="{ active }">
    <div class="carried">
      <DragPreview :id="active.id" />
      <span v-if="saving" class="saving">Saving…</span>
    </div>
  </template>
</DnDProvider>

Déposer en dehors de toute cible

Faites glisser une application hors du dock et relâchez, dans chaque mode.

Live

Chargement de la démo…

<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider, type DropOutside } from '@vue-dnd-kit/core';
import Dock from './Dock.vue';

const MODES: DropOutside[] = ['placeholder', 'cancel', 'remove'];
const dropOutside = ref<DropOutside>('remove');
</script>

<template>
  <DnDProvider>
    <div class="demo">
      <fieldset class="modes">
        <legend>dropOutside</legend>
        <label v-for="value in MODES" :key="value" :class="{ on: dropOutside === value }">
          <input v-model="dropOutside" type="radio" :value="value" />
          {{ value }}
        </label>
      </fieldset>
      <code class="call">makeSortableList(dock, { strategy: 'horizontal', dropOutside: '{{ dropOutside }}' })</code>
      <Dock :drop-outside="dropOutside" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.modes {
  display: inline-flex;
  margin: 0;
  padding: 3px;
  border: 0;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

legend {
  float: left;
  align-self: center;
  padding: 0 8px 0 6px;
  font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.55;
}

.modes label {
  padding: 5px 10px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.modes label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

.modes label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

.modes input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.call {
  max-width: 100%;
  padding: 6px 10px;
  overflow-x: auto;
  border-radius: 8px;
  background: color-mix(in srgb, currentColor 6%, transparent);
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  color: #42b883;
}
</style>
Dock.vue
const { items } = makeSortableList(dock, {
  items: apps,
  strategy: 'horizontal',
  dropOutside: () => props.dropOutside,
  // Called on the list the items leave, with `to.index` -1, when they are removed.
  onDrop: (e) => {
    if (e.to.index === -1) notice.value = `Removed ${e.items.map((app) => app.name).join(', ')}`;
  },
});
dropOutsideUn dépôt en dehors de toute cible…
'placeholder'Atterrit là où se trouve le placeholder : la dernière place que la liste a montrée. La valeur par défaut.
'cancel'Est annulé : les items reviennent à leur place. La valeur par défaut avec placeholder: 'indicator'.
'remove'Retire les items de leur liste. L'onBeforeDrop de la liste est interrogé d'abord, et son onDrop reçoit un to.index de -1.

Il est lu au dépôt : une ref ou un getter peut donc le changer. Une corbeille fait la même chose volontairement : makeDroppable(trash, { onDrop: (e) => e.source?.remove() }). Voir le glisser-déposer.