デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import TaskTable from './TaskTable.vue';
</script>
<template>
<!-- The row in hand: a table row can't have a shadow, so its overlay content has it. -->
<DnDProvider overlay-class="overflow-hidden rounded-md bg-default shadow-xl ring ring-accented">
<TaskTable />
</DnDProvider>
</template>Nuxt UI の UTable がテーブル、その行、その仮想化ライブラリを描画します:DOM にあるのは画面上の行だけです。Vue DnD Kit はそれを並べ替えます。テーブルは UI ライブラリが提供するそのままのもので、スロットも ui クラスもそのまま使っています。書き直した部分はありません。
注目ポイント
TaskTable.vue:並べ替え可能なリストはテーブルの<tbody>で、UTableが公開するtableRefから取得します。テーブルはリストのドラフトをdataとして受け取るので、ドラッグ中に行が動きます。get-row-idはリストと同じく、タスクごとに行のキーを決めます。axis: 'y'はドラッグをその列の中に、restrictToFirstScrollableAncestorはテーブルの中に留めます:テーブルのスクロール要素が、行を囲む最初のスクロール可能な要素です。デモを展開すると、テーブルが画面いっぱいに広がります。RowGrip.vue:行を描画するのはUTableなので、あなたのコンポーネントが行になることはありません。最初のセルにあるグリップ(#grip-cellスロット)が、自分を囲む行をアイテムにし、そのハンドルになります。- 検索はリストの
filterです:テーブルは一致するタスクを表示し、ドロップすると配列全体が書き戻され、隠れたタスクは元の位置に残ります。 tasks.ts:2,000 件のタスクをshallowRefに入れています:ライブラリがその中身を見ることはありません。
テーブルが描画する行
グリップは、行を返すゲッターを渡して makeSortableItem を呼び出します:描画された後の、グリップを囲む <tr> です。ハンドルなので、フォーカスと ARIA 属性はグリップが受け取り、行の残りの部分はクリックやテキストの選択に自由に使えます。
const grip = useTemplateRef<HTMLElement>('grip');
makeSortableItem(() => grip.value?.closest('tr') ?? null, {
id: () => props.task.id,
handle: grip,
label: () => props.task.title,
});
<UTable ref="table" :data="items" :get-row-id="(task: Task) => task.id" …>
<template #grip-cell="{ row }">
<RowGrip :task="row.original" />
</template>
</UTable>
オーバーレイはライブラリが作る行のコピーで、セルの幅を保った専用のテーブルに入っています。テーブルの行は影を描かないので、プロバイダーの overlay-class がオーバーレイの中身に影を付け、テーブルの背景とリングも加えます。行が移動している間は、行の ui クラスがその位置に色を付けます(data-[dnd-placeholder]:bg-primary/5)。
<DnDProvider overlay-class="overflow-hidden rounded-md bg-default shadow-xl ring ring-accented">
<TaskTable />
</DnDProvider>
仮想化された行
virtualize はテーブルに TanStack Virtual を与えます:画面上の行とその周りの 8 行(overscan)が、2 つのスペーサー行の間に描画されます。
<UTable
:data="items"
:watch-options="{ deep: false }"
:virtualize="{ estimateSize: 45, overscan: 8, onChange: () => captureLayout() }"
class="min-h-0 flex-1"
/>
estimateSizeは最初の見積もりです:仮想化ライブラリは各行を描画するときに計測します。計測するとその下の行が動くので、onChangeがリストのcaptureLayout()を呼び出します。こうしてドラッグ中は、その移動が一瞬で飛ぶのではなく、ライブラリによってアニメーションされます。- テーブルがスクロール要素で、手に持った行はその中に留まります:上端や下端の近くではドラッグがテーブルをスクロールし、新しい行は見えてきたときに描画されます。
- ドラフトは移動のたびに新しい配列になるので、テーブルがその中身を監視する必要はありません(
deep: false)。 - 持っている行から遠くまでスクロールすると、その行は描画範囲から外れてアンマウントされます。ドラッグは続きます:オーバーレイはまだその行を表示しており、行はドラフトが決めた位置に着地します。自分で作る仮想リストなら、
rangeExtractorでその行をマウントしたままにできます(仮想リストを参照)。テーブルは行をスペーサー行の間に並べるので、ほかの行から離れて描画された行はそれらを押しのけてしまいます。そのため、このテーブルでは行をアンマウントさせています。
検索してからドラッグ
const { items, captureLayout } = makeSortableList(() => table.value?.tableRef?.tBodies[0] ?? null, {
items: tasks,
filter: (task) => {
const text = query.value.trim().toLowerCase();
return !text || `${task.id} ${task.title} ${task.assignee}`.toLowerCase().includes(text);
},
axis: 'y',
modifiers: [restrictToFirstScrollableAncestor],
label: 'Tasks',
});
「Mia」で検索し、彼女のタスクの 1 つを別のタスクの上へ移動してから、検索をクリアしてみましょう:彼女のタスクは新しい順序になり、ほかのタスクはすべて元の位置のままです。ドロップの e.to.index は表示されている行で数えます。e.result は配列全体を持っています。
- 行。行全体を描画するスロット(Vuetify の
#item、Quasar の#body)からは行の要素が得られます:それをアイテムにします。セルしか得られない場合(PrimeVue のColumnの#body、Element Plus の列のデフォルトスロット、Naive UI のrender)は、そのうちの 1 つにグリップを置き、ここと同じようにそのclosest('tr')をアイテムにします。 - リスト。テーブルのルート要素の中の
<tbody>です:テーブルに付けたテンプレート参照でコンポーネントが、その$elで要素が得られます。 - キーとデータ。行のキーを自分の id にし(
item-value、row-key、dataKey)、リストのドラフトをテーブルのデータとして渡します。 - テーブル独自の並べ替えはオフ。テーブル自身の行の並べ替えと列のソートは使いません:順序はリストのものです。
- 仮想テーブルは行をスペーサーの間か、絶対配置のボックスに置きます:どちらでも動き、ドラッグは行がアンマウントされても続きます。
@nuxt/ui/vite プラグイン、Tailwind CSS、サンプルを囲む UApp)でファイルを開きます。@nuxt/ui モジュールを入れた Nuxt アプリでは、フォルダーをコピーしてページで index.vue を描画してください。キーボードとスクリーンリーダー
Tab でタスクのグリップに移り、:kbd{value="Space"} を押して行を持ち上げます。:kbd{value="ArrowUp"} と ArrowDown で動かし、:kbd{value="Space"} でドロップし、:kbd{value="Escape"} で元に戻します。行には label があり、リストにもあります:スクリーンリーダーが聞くのはキーではなく「Fix the login form dropped at position 3 of 2000 in Tasks.」です。
さらに読む:仮想リスト · アイテムの一部を表示する · スタイリング:Tailwind と UI キット · アクセシビリティ