並べ替え可能なリストではないものには、すべて makeDraggable と makeDroppable を使います:ゴミを分別箱へ、家具を間取り図の上へ、ファイルをフォルダーへ、ゴミ箱、チェス盤。
デモを読み込み中…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type Key } from '@vue-dnd-kit/core';
import Bin from './Bin.vue';
import { BINS, WASTE } from './waste';
// The tray takes everything back (no `accept`); the bins take what their rule allows.
const TRAY = { id: 'tray', name: 'To sort', color: '#42b883', accept: '*', rule: "'*'" };
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<DnDProvider>
<div class="recycling">
<Bin :bin="TRAY" :items="itemsIn('tray')" tray @drop="drop('tray', $event)" />
<div class="bins">
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.recycling {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 600px;
margin: 0 auto;
}
.bins {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
@media (max-width: 560px) {
.bins {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>ドラッグ可能な要素
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ waste: { id: string; name: string; type: string } }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
id: () => props.waste.id, // read back as `active.id`
type: () => props.waste.type, // matched by the targets' `accept`
data: () => props.waste, // anything: `active.data`
});
</script>
<template>
<div ref="el" :class="{ dragging: isDragging }">{{ waste.name }}</div>
</template>
id はデータから、ゲッターとして渡します。デフォルトの id は useId() から生成され、コンポーネントのインスタンスごとに新しくなるため、アプリの他の部分にとっては意味を持ちません。オプションは必要になった時点で読み取られるので、ゲッターや ref を使えば常に最新の値になります。
| オプション | 役割 |
|---|---|
id、data、type | 何がドラッグされるか、何を運ぶか、ターゲットの accept が何と照合するか。 |
handle | 内側のセレクターまたは要素:それだけがドラッグを開始します。ハンドルを参照。 |
disabled | ドラッグを開始しません。 |
overlay | true(デフォルト):コピーがポインターに追従します。コンポーネント:独自のプレビュー。false:要素自体が動きます(後述)。 |
axis、modifiers | ドラッグを 'x' または 'y' に固定する、スナップする、境界内に収める:制約を参照。 |
keyboardMode | 矢印キーでの動かし方:ピクセル単位(デフォルト)か、ターゲットからターゲットへ(後述)。 |
activation、canDrag | 押下がいつドラッグになるか:センサーを参照。 |
attributes | キーボードとスクリーンリーダー向けの role、tabindex、aria-*(デフォルト true)。 |
| 戻り値 | 内容 |
|---|---|
isDragging | この要素がドラッグされています。 |
selected | 書き込み可能で、プロバイダーの選択にバインドされています。 |
transform | ドラッグのオフセット。overlay: false 用です。 |
id | 使用中の id。 |
ボタン、入力欄、その他の操作可能な子要素はポインタードラッグを開始しないので、ドラッグ可能な要素の中でもそのまま動作します。すべてのオプションは makeDraggable API にあります。
ドロップターゲット
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type Key } from '@vue-dnd-kit/core';
import type { Bin, Waste } from './waste';
const props = defineProps<{ bin: Bin; items: Waste[] }>();
const emit = defineEmits<{ drop: [id: Key] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isOver, isAccepting } = makeDroppable(el, {
id: props.bin.id,
accept: props.bin.accept, // 'glass', ['paper', 'cardboard'], a function…
onDrop: (event) => emit('drop', event.active.id),
});
</script>
<template>
<section ref="el" :class="{ accepting: isAccepting, over: isOver && isAccepting, refused: isOver && !isAccepting }">
<WasteItem v-for="waste in items" :key="waste.id" :waste="waste" />
</section>
</template>
| 戻り値 | true になる条件 |
|---|---|
isOver | ドラッグがこのターゲットの上にある。受け入れるかどうかは問いません。 |
isAccepting | 何かがドラッグされていて、このターゲットがそれを受け入れる:有効なターゲットを強調表示するのに使います。 |
placement | 真偽値ではありません:{ top, right, bottom, left }、ポインターがターゲットのどの半分にあるか。ホバーされていなければ null。「このカードの前 / 後」に使います。 |
isAccepting なしの isOver は、ドラッグが受け入れないターゲットの上にあることを意味します。上の分別箱のように、それを表示しましょう。そこでのドロップはキャンセルされ、アイテムは元の位置に戻ります。アイテムを運んでいる間、受け入れられない箱は薄くなります。ほかの見せ方はスタイリングにあります。
| オプション | 役割 |
|---|---|
id、data | over.id と over.data として読み取れます。 |
accept、acceptMode | どのドラッグを受け入れるか(次のセクション)。 |
disabled | 何も受け入れず、ドラッグはその上を通り過ぎます。 |
priority | ネストせずに重なったターゲットでは、値が大きいほうが優先されます(デフォルト 0)。 |
onDragEnter、onDragLeave | ドラッグが入った、または出た。 |
onBeforeDrop、onDrop | ドロップを判断し、適用します(後述)。 |
ターゲットが受け入れるドラッグ
ドラッグ可能な要素の type は、ターゲットの accept と照合されます。
accept | 受け入れるもの |
|---|---|
省略または '*' | すべて。 |
'glass' | glass 型のドラッグ可能な要素と、型のないもの。 |
['paper', 'cardboard'] | どちらかの型と、型のないもの。 |
({ active, items, types }) => boolean | 関数が決めたもの。 |
type のないドラッグ可能な要素は、どの文字列のルールにも合致します。type は配列にもできます(['paper', 'large']):1 つでも合致すれば十分です。関数なら、ドラッグに関するどんな情報でも判断に使えます。上のコンポスト箱は、アイテムの型ではなくデータを読み取っています。
{
id: 'compost',
accept: ({ items }) => items.every((item) => (item.data as Waste).compostable),
}
accept は、ドラッグがターゲットを探している間にその場で読み取られます。ref を渡せば変更できます。
複数のアイテムを同時にドラッグする場合、acceptMode はいくつ合致する必要があるかを指定します:'all'(デフォルト)または 'some'。'some' では、残すアイテムの id を onBeforeDrop から返すと、それ以外は元の位置に戻ります。ドロップの判断を参照してください。
ドロップ
通常のドロップターゲットの onDrop は、あなたがデータを変更する場所です。ライブラリが自分で何かを動かすことはありません。
<script setup lang="ts">
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</template>
| イベントのフィールド | 内容 |
|---|---|
active | ドラッグ:id、ids、items(それぞれ { id, data, type, item })、data、type。 |
over | このターゲット:id、data、element、rect、accepted。 |
pointer、delta | ポインターの位置と、開始位置からの移動量。 |
position | ドラッグ中のボックスがこのターゲットの中のどこにあるか(次のセクション)。 |
source | アイテムが並べ替え可能なリストから来た場合に設定されます:{ list, remove() }。 |
active.ids にはドラッグ中のすべての id が入り、掴んだものが先頭です。順序は onBeforeDrop(判断)、onDrop(適用)、そしてプロバイダーの drag-end です。ページのすべてのドラッグを 1 か所で受け取るには useDnDMonitor を使います(プロバイダーを参照)。ドロップの前に確認する、ドラッグを続ける、一部のアイテムだけを受け入れるには、ドロップの判断を参照してください。
ドロップした場所に置く
間取り図、キャンバス、ボード:アイテムは離した場所にとどまります。どのイベントも position を持ちます。これは、絶対配置したアイテムをドロップした位置に置くための left / top です。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Planner from './Planner.vue';
</script>
<template>
<DnDProvider>
<Planner />
</DnDProvider>
</template>const room = useTemplateRef<HTMLElement>('room');
makeDroppable(room, {
id: 'room',
onDrop: ({ active, position }) => {
const piece = find(active.id);
if (!piece || !position || !room.value) return;
// Clamped, so a piece let go half outside stays inside the walls.
piece.x = clamp(position.x, room.value.clientWidth - piece.width);
piece.y = clamp(position.y, room.value.clientHeight - piece.height);
piece.place = 'room';
},
});
<div ref="el" class="piece" :style="{ left: `${piece.x}px`, top: `${piece.y}px` }">…</div>
position は over の中でのドラッグ中のボックスの左上の角で、パディングの端から測り、スクロール量を加えた値です。部屋のボーダー、つまり壁は含まれません。何もない上では null です。
オーバーレイなし
デフォルトでは要素のコピーがポインターに追従し、要素自体はその場にとどまります:倉庫にある家具がそうです。overlay: false ではコピーがなく、ドラッグが返す transform で要素自体が動きます:部屋の中の家具がそうです。
const inRoom = computed(() => props.piece.place === 'room');
const { isDragging, transform } = makeDraggable(el, {
id: () => props.piece.id,
overlay: computed(() => !inRoom.value), // a value or a ref, read when the drag starts
});
const style = computed(() => ({
left: `${props.piece.x}px`,
top: `${props.piece.y}px`,
...(transform.value && { transform: `translate3d(${transform.value.x}px, ${transform.value.y}px, 0)` }),
}));
- このとき
positionは要素自身の角になるので、ドロップのハンドラーはそのままで構いません。 - 動いている間、要素はポインターの下にあります。ターゲットを探すとき、ライブラリはそれを透過して見ます。ドラッグ中は隣の要素より上に表示してください(
z-index)。 - 要素はページと一緒にスクロールしますが、オーバーレイはしません。
transformはドラッグ開始以降にコンテナーが動いた分を差し引くので、ページがスクロールしても要素はポインターの下にとどまります。 - 要素は自身のコンテナーの中で動きます。
overflow: hiddenの親に切り取られ、どの要素の上を通るかはスタッキングコンテキストで決まります。オーバーレイにはどちらの問題もありません。
コピーの代わりに独自のプレビューを使うには、オーバーレイを参照してください。
ゴミ箱ゾーン
並べ替え可能なリストから始まったドラッグは e.source を持ちます。remove() を呼ぶと、リストはそのアイテムを取り除きます。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Triage from './Triage.vue';
</script>
<template>
<DnDProvider>
<Triage />
</DnDProvider>
</template>const triage = (bin: 'done' | 'deleted') => (e: DnDEvent) => {
bins[bin].push(...e.active.items.map((entry) => entry.item as Notice));
e.source?.remove();
};
makeDroppable(doneZone, { id: 'done', onDrop: triage('done') });
makeDroppable(trashZone, { id: 'trash', onDrop: triage('deleted') });
e.active.items[i].itemはリストのアイテムそのものです。保持しておけば、元に戻すときに元の位置へ戻せます。- その後、リスト自身の
onDropがto.index === -1で実行され、e.resultにはアイテムを除いたリストが入ります。 - アイテムがもうどこにも表示されないので、オーバーレイはゾーンの中へ縮んでいきます。
- リストは、すべての外にドロップされたものを取り除くこともできます:
dropOutsideを参照。
Delete キーのほうが速いので、そのいずれかも用意してください。並べ替え可能なアイテムへのドロップ
並べ替え可能なリストの行もドロップターゲットにできます:ブックマークを受け入れるフォルダーや、タスクを受け入れるグループなどです。アイテムの要素にドロップ可能な設定を追加します。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Bookmarks from './Bookmarks.vue';
</script>
<template>
<DnDProvider>
<Bookmarks />
</DnDProvider>
</template>const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.folder, type: 'folder' });
const { isOver } = makeDroppable(el, {
accept: 'link',
onDrop: (e) => {
emit('add', e.active.items.map((entry) => entry.item as Link)); // Bookmarks adds them
e.source?.remove(); // take them out of the list they came from
},
});
開いたフォルダーは、リンクを同じグループのリストとして表示します。このリストはリンクだけを受け入れます:リンクはそこで並べ替えられ、トップレベルやほかのフォルダーへドラッグして戻せます。
リストがそのドラッグを並べ替えようとしている間、このようなターゲットが有効になるのはアイテムの中央(リストの軸方向で 25〜75%)だけです。そこでのドロップはアイテムの中に入り、リストは動きません。端の近くでは、リストはいつもどおり並べ替えます。
acceptがドラッグを拒否するターゲットはスキップされます。リストはそのアイテムの上でも、ほかのアイテムと同じように並べ替えます。フォルダーはリンクしか受け入れないので、ドラッグしたフォルダーは、ほかのフォルダーを越えて並べ替えられます。- ターゲットが自分自身のドラッグを受け取ることはありません。
- キーボードでは、矢印キーはまずフォルダーで止まり、同じ方向にもう一度押すとそれを越えて進みます。
- ドロップ後はアイテムが表示されなくなるので、オーバーレイは元の位置に戻る代わりにフォルダーの中へ縮んでいきます。
- フォルダーの上では、リストはドラッグした行の位置を元の場所に保つので、ポインターの下で何もずれません。このときその位置には
data-dnd-placeholder="into"が付きます:ドロップはそこには入らないので、必要なら隠してください([data-dnd-placeholder='into'] { opacity: 0 })。
ファイルマネージャーは同じことをグリッドで、入れ子のフォルダーとともに行っています。
通常のドラッグ可能な要素をリストへ
並べ替え可能なリストは、makeDraggable でドラッグされた要素からアイテムを作る方法を知っていれば、それを受け入れます。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Agenda from './Agenda.vue';
import BlockChip from './BlockChip.vue';
const BLOCKS = [
{ title: 'Focus', minutes: 90, color: '#42b883' },
{ title: 'Meeting', minutes: 30, color: '#3b82f6' },
{ title: 'Lunch', minutes: 60, color: '#f59e0b' },
{ title: 'Gym', minutes: 45, color: '#ec4899' },
{ title: 'Break', minutes: 15, color: '#a1a1aa' },
];
</script>
<template>
<DnDProvider>
<div class="planner">
<aside class="blocks">
<h4>Blocks</h4>
<BlockChip v-for="block in BLOCKS" :key="block.title" :block="block" />
</aside>
<Agenda />
</div>
</DnDProvider>
</template>
<style scoped>
.planner {
display: flex;
justify-content: center;
gap: 24px;
}
.blocks {
display: flex;
flex: none;
flex-direction: column;
gap: 6px;
width: 140px;
}
h4 {
margin: 0 0 6px;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
@media (max-width: 480px) {
.blocks {
width: 112px;
}
}
</style>const { items } = makeSortableList(list, {
items: agenda,
createItem: (active) => ({ ...(active.data as Block), id: `entry-${++created}` }),
});
ドラッグ可能な要素がリストの上にある間、作成されたアイテムはドラフトの中にあり、ほかの行と同じようにテンプレートで描画されます。予定表の時刻はすでにそれを数に入れています。ドロップすると実際のアイテムになり(mode: 'insert'、from.list === null)、オーバーレイはその上に着地します。
- 拒否するには
undefinedを返します。createItemのないリストは通常のドラッグ可能な要素を無視します。 - 新しいアイテムには独自のキーを与えてください。ブロックはパレットに残ります。
- キーボードでは、リストに向かう矢印キーでドラッグ可能な要素がその高さでリストに取り込まれ、その後の矢印キーで枠ごとに動きます。
コンピューターからのファイル
デスクトップやファイルマネージャーからドラッグされてきたファイルは、ライブラリのドラッグではありません:運んでいるのはブラウザーです。ドロップターゲットは external でそれを受け取り、ドロップすると e.external に入って届きます。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import AvatarDrop from './AvatarDrop.vue';
import Attachments from './Attachments.vue';
</script>
<template>
<DnDProvider>
<div class="form">
<AvatarDrop />
<Attachments />
</div>
</DnDProvider>
</template>
<style scoped>
.form {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
gap: 16px;
}
</style>const { isOver, isAccepting } = makeDroppable(el, {
external: { files: 'image/*', multiple: false },
onDrop: (e) => show(e.external!.files[0]!),
});
filesは<input accept>と同じ書き方です:MIME タイプ(image/*、application/pdf)と拡張子(.md)。trueはすべてのファイルを受け取り、関数ならファイルごとに判断します。linksとtextはほかのアプリやタブからドラッグされたものを、typesはそのほかの MIME タイプを受け取ります。- このドラッグはプロバイダーのドラッグとして動きます:
isOver、isAccepting、イベント、読み上げは、ほかのドラッグと同じように働きます。externalのないターゲットはこれを無視します。オーバーレイはありません:ドラッグ中の画像はブラウザーが描きます。 - ファイルがページの上にある間、ブラウザーが教えるのはタイプだけです(
e.external.fileTypes)。名前、サイズ、内容はドロップとともに届き、拡張子もそのときに確認されます。フォルダーは除外されます。multiple: falseでは、複数ファイルのドラッグは拒否されます。
リストもファイル、リンク、テキストを受け取り、ドラッグ中にそれらのための場所を空けます。アイテムはページの外へ、ほかのアプリやあなたのアプリの別のウィンドウへ出ていくこともできます:ファイルとほかのアプリを参照してください。
<input type="file"> を用意してください。キーボード
フォーカスされたドラッグ可能な要素は Space または Enter で持ち上げ、同じキーでドロップし、:kbd{value="Escape"} でキャンセルします。その間は矢印キーで動かします。
keyboardMode | 矢印キー 1 回で |
|---|---|
'pixels'(デフォルト) | 25 px(プロバイダーの keyboard.step)動きます。:kbd{value="Shift"} を押すと 4 倍の距離です。家具はこのように動きます。 |
'droppables' | その方向で受け入れてくれる最も近いターゲットへジャンプします。ゴミは箱から箱へジャンプし、拒否する箱は飛ばします。 |
makeDraggable(el, { id: () => props.waste.id, type: () => props.waste.type, keyboardMode: 'droppables' });
キーはプロバイダーで割り当て直せます。詳しくはアクセシビリティを参照してください。