ドラッグは 1 つの軸に固定したり、ボックスの中に収めたり、グリッドにスナップしたり、独自のルールで曲げたりできます。こうしたルールがモディファイアで、毎フレーム、ドラッグのオフセットを調整します。1 つのドラッグ可能な要素、リスト、ページの一部のいずれかに設定すると、オーバーレイ、ドロップターゲット、ドロップのすべてがそれに従います。
軸を固定する
タイムライン上のクリップはトラックに沿ってスライドします。固定をオフにすると、トラックを移ることもできます。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Timeline from './Timeline.vue';
</script>
<template>
<DnDProvider>
<Timeline />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: () => props.clip.id,
axis: () => (props.locked ? 'x' : undefined),
});
axis: 'x' は横方向だけ、'y' は縦方向だけに動きます。makeDraggable、makeSortableList、makeSortableTree、makeSortableItem、makeDragArea のいずれもこれを受け取ります。
makeSortableList(listRef, { items: tracks, axis: 'y' });
- 固定はすべてのモディファイアの後に実行されるので、モディファイアが何をしても保たれます。スナップや境界がアイテムを線から押し出すことはありません。
- 線は画面上ではなくコンテンツの中にあり、ドラッグを開始したリスト(または親要素)と一緒に動きます。ドラッグの途中でページをスクロールしても、アイテムは自分の行にとどまります。
- アイテム自身の
axisはリストやツリーのものより優先され、ドラッグ可能な要素(またはそのリスト)のものはドラッグエリアのものより優先されます。 - ドラッグ開始時に読み取られるので、上のようにゲッターで切り替えられます。
'y'に固定したツリーでもネストはできます。そのときの深さは、行の中央へのホバーで決まります。
restrictToVerticalAxis や restrictToHorizontalAxis モディファイアより axis を使ってください。同じことをしますが、通常のモディファイアとしてチェーンの途中で実行されるため、その後のモディファイア(両軸をスナップするグリッド)がアイテムを軸から外すことがあります。モディファイア
モディファイアは毎フレーム、ドラッグのオフセットを調整します。その結果はオーバーレイを動かすと同時に、ターゲットを探す点にもなります。そのため、リストは固定・制限・スナップされた位置で並べ替えられ、ドロップの position はボックスが最終的に置かれた場所になります。
| モディファイア | 役割 |
|---|---|
restrictToParent | オーバーレイを、ドラッグが始まったコンテナー(並べ替え可能なリスト、または通常のドラッグ可能な要素の親要素)の中に収めます。 |
restrictToElement(target) | オーバーレイを要素の中に収めます:テンプレート参照、ゲッター、または要素そのもの。 |
restrictToWindowEdges | オーバーレイをウィンドウの中に収めます。 |
restrictToFirstScrollableAncestor | オーバーレイを、ドラッグされた要素の最初のスクロール可能な祖先の中に収めます。 |
snapToGrid(size) | オーバーレイをグリッドにスナップします(後述)。 |
snapCenterToCursor | 掴んだ位置のオフセットを保つ代わりに、オーバーレイをポインターの下の中央に置きます。 |
restrictToVerticalAxis | 縦方向だけに動きます。axis: 'y' を推奨します。 |
restrictToHorizontalAxis | 横方向だけに動きます。axis: 'x' を推奨します。 |
modifiers は、プロバイダー、ドラッグエリア、リストとツリー、ドラッグ可能な要素と並べ替え可能なアイテムのオプションです。プロバイダー以外はすべて ref やゲッターも受け取り、ドラッグ開始時に読み取られます。
内側に収める
ビデオ通話のセルフビューは画面の中にとどまります。境界を切り替えて、遠くへドラッグしてみましょう。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import CallScreen from './CallScreen.vue';
</script>
<template>
<DnDProvider>
<CallScreen />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: 'self-view',
modifiers: () => props.modifiers, // [restrictToParent], [restrictToWindowEdges] or []
});
restrictToParentは、親要素である画面の上に保ちます。並べ替え可能なアイテムの場合、「親」は DOM のどこにあってもそのリストです。restrictToWindowEdgesは画面の外には出られますが、ウィンドウの外には出られません。画面の外でドロップすると、ドロップターゲットの上ではないので元の位置に戻ります。- 境界がなければ、どこへでも行けます。
境界はオーバーレイを、そのときのサイズで内側に収めます。overlayFit: 'slot' では、幅の広いアイテムのリストの上で大きくなったボックスは、新しいサイズで内側に収められます。
グリッドにスナップする
フローチャートのノードはキャンバスのグリッドにスナップします。セルのサイズと滑らかな移動を切り替えてみましょう。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Flowchart from './Flowchart.vue';
</script>
<template>
<DnDProvider>
<Flowchart />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: () => props.node.id,
modifiers: () => [snapToGrid({ x: props.grid, y: props.grid, smooth: props.smooth })],
});
snapToGrid(24) は両軸を 24 px のセルにスナップします。オブジェクトを渡すと、さらに設定できます。
| オプション | 意味 |
|---|---|
x、y | 軸ごとのセルのサイズ(ピクセル)。片方を省くと、もう片方だけがスナップします:{ y: 15 } はイベントを 15 分単位にスナップし、日をまたぐ方向には自由に動かせます。 |
origin | 'container'(デフォルト):オーバーレイの左上の角が、コンテナー(並べ替え可能なリスト、または通常のドラッグ可能な要素の親)の左上から引いたグリッド線の上に着地します。'delta':オフセット自体を丸めるので、グリッドから外れた位置で始まったアイテムは外れたまま、元の位置からセル単位で動きます。どこにでも置かれ(自由なキャンバス)、それぞれの揃え方を保つべきアイテムに選んでください。 |
smooth | true または { duration, easing }:オーバーレイがセルの間を滑らかに移動します(デフォルト 80 ms)。ターゲットはスナップされた位置をすぐに使います。 |
origin: 'container' では、グリッドをコンテナーのボーダーボックスから描いてください。上のキャンバスは枠を border ではなく box-shadow で描いているので、グリッド線はちょうど端から始まります。
独自のモディファイア
スライド上のボックスは、ほかのボックスに近づくとその端や中心にぴたりと合い、ピンクのガイドが揃った線を示します。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Slide from './Slide.vue';
</script>
<template>
<DnDProvider>
<Slide />
</DnDProvider>
</template>import type { Modifier } from '@vue-dnd-kit/core';
/** Near a line, the box jumps onto it. */
const magnet: Modifier = ({ transform, overlayRect, containerRect, active }) => {
if (!containerRect) return transform;
const id = String(active.id);
const left = overlayRect.left - containerRect.left;
const top = overlayRect.top - containerRect.top;
return {
x: transform.x + pull(left, overlayRect.width, lines(id, 'x')),
y: transform.y + pull(top, overlayRect.height, lines(id, 'y')),
};
};
// In order: the magnet pulls, then the slide's edges hold.
const modifiers = [magnet, restrictToParent];
モディファイアは transform(それより前のモディファイアが残した、ドラッグ開始からのオフセット)を受け取り、新しいオフセットを返します。計算に必要になりうる情報も受け取ります。
| 引数 | 内容 |
|---|---|
transform | ここまでのオフセット。 |
overlayRect | transform で動かしたオーバーレイのボックス。現在のサイズです。 |
activeRect | ドラッグ開始時のドラッグされた要素。 |
containerRect | ドラッグが始まったコンテナー(並べ替え可能なリスト、または親要素)の現在の位置。スクロールに追従します。 |
windowRect | ウィンドウ。 |
origin | ドラッグ開始時のポインター。 |
active、over | ドラッグと、その下にあるターゲット。 |
モディファイアは毎フレーム実行されます。高速で副作用のないものにしてください。上のガイドは、モニターの onDragMove でドラッグの delta(すべてのモディファイア適用後のオフセット)から描いています。型はモディファイアにあります。
順序
複数の場所のモディファイアは順番に実行され、それぞれが前の結果から計算します。
- プロバイダーの
modifiers - ドラッグが始まったドラッグエリアの
modifiers - ドラッグされたアイテム自身の
modifiers、なければそのリストまたはツリーのもの - ドラッグエリアの
restrictToArea axisの固定
1 つの配列の中では順番に実行されるので、スナップやマグネットは境界より前に置いてください。最後の境界は、ほかが何をしてもボックスを内側に収めます。上のスライドは [magnet, restrictToParent] を実行しています。逆順だと、マグネットがスライドの端で止められたボックスを数ピクセル外へ引っ張ってしまうことがあります。
アイテムの modifiers はリストのものを置き換えます。追加されるのではありません。エリアの境界はステップ 1〜3 のスナップの後に来るので、スナップされたアイテムも内側にとどまり、軸の固定はすべての最後に来ます。
境界のあるリストでも両端に届く
レイヤーパネル:行は上下にしか動かず、パネルから出ることはありません。一番上の行を一番下までドラッグしてみましょう。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Layers from './Layers.vue';
</script>
<template>
<DnDProvider>
<Layers />
</DnDProvider>
</template>const { items } = makeSortableList(list, { items: layers, axis: 'y', modifiers: [restrictToParent] });
ポインターが動き続けても、境界はオーバーレイをボックスの端で止めます。ポインターがリストの軸方向でオーバーレイから出ると、リストは代わりにポインターで並べ替えるので、アイテムをどう掴んでいても先頭と末尾の位置に届きます。グリッドはオーバーレイで並べ替え続けます。端で止められた時点で、すでに端のセルを覆っているためです。
ページの一部に対するルール
2 つのエリア、2 組のルール。Layout のステッカーはグリッドにスナップし、そこから出られません。Sketch のステッカーはどこへでも、Layout の中へも行けます。ドラッグは始まったエリアのルールを保ち、ルールが有効になっているエリアが強調表示されます。
デモを読み込み中…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider, type Coordinates, type Key } from '@vue-dnd-kit/core';
import DragArea from './DragArea.vue';
const stickers = ref([
{ id: 'logo', text: 'Logo', color: '#42b883', area: 'sketch', x: 20, y: 24 },
{ id: 'photo', text: 'Photo', color: '#3b82f6', area: 'sketch', x: 110, y: 96 },
{ id: 'quote', text: 'Quote', color: '#ec4899', area: 'sketch', x: 36, y: 150 },
{ id: 'title', text: 'Title', color: '#f59e0b', area: 'layout', x: 24, y: 24 },
{ id: 'button', text: 'Button', color: '#a855f7', area: 'layout', x: 120, y: 144 },
]);
const place = (area: string, id: Key, at: Coordinates) => {
const sticker = stickers.value.find((item) => item.id === id);
if (!sticker) return;
sticker.area = area;
sticker.x = Math.round(at.x);
sticker.y = Math.round(at.y);
};
</script>
<template>
<DnDProvider>
<div class="studio">
<DragArea
id="sketch"
title="Sketch"
rules="no rules"
:stickers="stickers.filter((sticker) => sticker.area === 'sketch')"
@place="place"
/>
<DragArea
id="layout"
title="Layout"
rules="snapToGrid(24) · restrictToArea"
strict
:stickers="stickers.filter((sticker) => sticker.area === 'layout')"
@place="place"
/>
</div>
</DnDProvider>
</template>
<style scoped>
.studio {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 12px;
width: 100%;
max-width: 560px;
margin: 0 auto;
}
</style><script setup lang="ts">
const area = useTemplateRef<HTMLElement>('area');
const { isDragging } = makeDragArea(area, {
modifiers: () => (props.strict ? [snapToGrid(24)] : []),
restrictToArea: () => props.strict,
});
</script>
<template>
<div ref="area" class="area" :class="{ busy: isDragging }">
<Sticker v-for="sticker in stickers" :key="sticker.id" :sticker="sticker" />
</div>
</template>
makeDragArea は、その要素の内側で始まるドラッグのルールを設定します——通常のドラッグ可能な要素にも並べ替え可能なアイテムにも適用され、それらは独自のルールを持つ必要がなくなります。アプリ全体で DnDProvider は 1 つにし、ボード、キャンバス、フィードにはそれぞれエリアを与えてください。
| オプション | 役割 |
|---|---|
axis | ドラッグを 1 つの軸に固定します。ドラッグ可能な要素自身の axis、またはそのリストのものが優先されます。 |
modifiers | プロバイダーのものの後、ドラッグ可能な要素自身のものの前に実行されます。 |
restrictToArea | true:ドラッグはエリアの要素の内側にとどまります。 |
autoScroll | エリアのスクロールコンテナー(要素自身、または最も近いスクロール可能な祖先)の自動スクロール。false でそこでの自動スクロールをオフにします。 |
activation | ポインターの種類ごとのアクティベーション。スクロールするフィードでタッチの押下を長くする場合など。ドラッグ可能な要素自身のものが優先され、プロバイダーのものは最後です。 |
disabled | エリアの中でも、その中のエリアでもドラッグが始まりません。そのドラッグ可能な要素は aria-disabled を示します。 |
どのオプションも値、ref、ゲッターを受け取り、押下やドラッグの開始時に読み取られます。エリアは isDragging(ここで始まったドラッグが実行中。上で枠が光っているのがそれです)と isAutoScrolling(自動スクロールがコンテナーを動かしている)を返します。完全なリファレンス:makeDragArea。
エリアの組み合わせ方
- ドラッグは、そこから出た後も、始まったエリアに従います。エリアの上を通っても何も変わりません。ただし
autoScrollは例外で、そのコンテナーをスクロールするすべてのドラッグに適用されます。 - 入れ子のエリアはマージされず、最も近いものが優先されます。例外は
disabledで、これは継承されます。オフになったエリアは、その中のエリアもオフにします。 - 衝突判定、キーボードのキー、読み上げはプロバイダー全体のままです。
DnDProvider で囲まないのはなぜでしょうか? プロバイダーは互いに独立していて、ドラッグが一方から他方へ渡ることはないので、ボードのアイテムはそこから出られなくなります。プロバイダーをネストするのは、ページの一部がプロバイダー全体に関わる独自の設定を必要とし、かつそのアイテムがその中にとどまる場合だけにしてください。プロバイダーを参照。ドロップした場所に置く
上のタイムライン、通話、フローチャート、スライド、エリアはどれも、各アイテムをドロップした場所に置いています。どのドラッグイベントも position を持ちます:ターゲットの中でのドラッグ中のボックスの左上の角で、パディングの端から測り、スクロール量を加えたもの——絶対配置したアイテムをちょうどそこに置くための left と top です。モディファイアの後に計算されるので、スナップされたノードはグリッド上に着地します。
makeDroppable(canvas, { id: 'canvas' });
useDnDMonitor({
onDragEnd({ active, over, position }) {
const node = nodes.value.find((item) => item.id === active.id);
if (!node || over?.id !== 'canvas' || !position) return;
node.x = Math.round(position.x);
node.y = Math.round(position.y);
},
});
自由なドラッグとドロップターゲットについて詳しくはドラッグ&ドロップを参照してください。