문서/가이드

멀티 드래그와 선택

클릭이나 선택 사각형으로 여러 항목을 선택해 함께 드래그합니다. 한 블록으로, 빈자리를 남기며, 또는 오버레이에 쌓아서 옮기고, 드롭할 때 일부만 받고, 직접 만든 오버레이로 보여 줍니다.

이 페이지 편집

이 페이지는 AI가 번역했으며 부정확한 내용이 있을 수 있습니다. 영어 원문 읽기

파일 몇 개를 선택한 뒤 그중 하나를 드래그해 보세요. 나머지도 함께 옵니다. multiple을 바꾸면 나머지 항목이 이동 중에 어떻게 움직이는지, 레이아웃을 바꾸면 그리드의 오버레이를 볼 수 있습니다.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Files />
  </DnDProvider>
</template>

선택

프로바이더마다 선택 하나, 즉 반응형 id 집합을 가집니다. 선택된 항목을 드래그하면 선택된 모든 항목이 드래그되며, 집은 항목이 처음입니다. 선택되지 않은 항목을 드래그하면 그 항목만 드래그됩니다.

사람들이 어떻게 선택하는지는 라이브러리가 정하지 않습니다. 클릭 규칙은 직접 정하세요. 라이브러리는 선택을 스스로 해제하지도 않으며, 가상 리스트가 재활용하는 행처럼 언마운트되어도 선택은 유지됩니다.

클릭: Ctrl/Cmd와 Shift

모든 정렬 가능한 항목과 draggable은 선택에 연결된 쓰기 가능한 ref인 selected를 반환합니다. 클래스나 체크박스(v-model="selected")에 바인딩하거나 클릭 핸들러에서 설정하세요.

FileItem.vue
<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>

데모에서는 리스트가 규칙을 가집니다. 클릭은 파일 하나를 선택하고, Ctrl/Cmd-클릭은 하나를 추가하거나 빼고, Shift-클릭은 마지막으로 클릭한 파일부터 연속으로 선택합니다.

Files.vue
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;
};

드래그를 끝내는 클릭은 @click에 전달되지 않으므로, 드롭이 실수로 선택을 바꾸는 일은 없습니다. 항목 동작에는 pointerup이 아니라 @click을 쓰세요.

어디서든: 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() });

선택은 id로 식별되므로 여러 리스트의 항목과 일반 draggable도 담을 수 있습니다. 드래그는 프로바이더 아래의 선택된 모든 항목을 가져갑니다.

사각형 선택

makeSelectionArea는 요소를 영역으로 만들어, 빈 공간을 누르면 선택 사각형이 그려지고 사각형이 지나는 항목이 선택되게 합니다. 이 바탕화면에서 아이콘 몇 개를 사각형으로 둘러싼 뒤 그중 하나를 드래그해 보세요.

Live

데모 불러오는 중…

<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>
Desktop.vue
<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>
  • 사각형은 직접 렌더링합니다. style이 영역의 콘텐츠를 기준으로 위치를 잡습니다.
  • 영역에는 static이 아닌 position이 필요합니다(relative면 됩니다). 그렇지 않으면 사각형이 엉뚱한 곳에 그려집니다.
  • 사각형은 영역 자체에서만 시작하므로 영역을 넓게 잡으세요. 항목에 꼭 맞는 래퍼가 아니라, 항목 주변에 여백이 있는 패널 전체로요.
  • 항목을 누르면 그 항목이 드래그됩니다. 버튼, 입력 필드, 영역의 스크롤바는 자신의 누르기를 그대로 받습니다.
  • 영역이나 페이지가 스크롤되는 동안에도 사각형은 콘텐츠에 고정됩니다. Escape는 사각형을 취소하고 선택을 되돌립니다.
  • 사각형을 그리는 동안 useDnD().status는 'selecting'입니다. 바탕화면 아래의 줄이 이를 읽습니다.
옵션하는 일
strategy'toggle'(기본값): 사각형이 지나는 항목의 선택이 뒤집힙니다. 'select': 사각형 안의 항목만 선택된 상태로 남으므로, 빈 공간을 클릭하면 영역의 항목 선택이 해제됩니다.
modifier그리려면 누르고 있어야 하는 키: { keys: ['Shift'], method: 'every' }(또는 'some').
accept사각형이 선택하는 draggable. type과 비교하는 accept 규칙입니다.
disabled사각형을 그리지 않습니다. 켜면 그리고 있던 사각형이 취소됩니다.
onSelected(ids)사각형이 끝날 때 선택과 함께 호출됩니다.

strategy, modifier, disabled는 값, ref, getter를 받으며, 누르기가 사각형을 시작할 때 읽습니다. 영역 안에 영역이 있으면 가장 안쪽 영역이 누르기를 받습니다. makeSelectionArea API 페이지에서 이 옵션들을 실시간으로 바꿔 볼 수 있습니다.

놓인 모양 그대로 옮기기

아이콘은 일반 draggable(makeDraggable)이며, 각자 자기 아이콘을 data로 가집니다. 오버레이는 데모가 직접 만든 컴포넌트로, 드래그 중인 모든 아이콘의 DragPreview를 집은 아이콘을 중심으로 바탕화면에 놓인 모양 그대로 배치합니다. 아이콘은 제자리에서 기다리며 isDragging으로 흐려지고, isDragging은 복사본이 안착할 때까지 true로 남습니다.

Formation.vue
<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>
DesktopIcon.vue
const { isDragging, selected } = makeDraggable(el, { id, data: () => props.icon, overlay: Formation });

드롭은 바탕화면이 받습니다. position은 집은 아이콘을 놓은 위치를 바탕화면 좌표로 나타낸 값입니다. 그 아이콘은 그곳으로 가고, 나머지도 같은 거리만큼 옮겨집니다. 그다음 각 복사본이 새 자리에 있는 자기 아이콘 위에 안착합니다. 바탕화면 밖에 놓으면 복사본은 아이콘으로 돌아갑니다.

Desktop.vue
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;
    }
  },
});

집을 때 클론은 기본적으로 최대 5개 항목까지만 만들어집니다. 프로바이더의 overlay-previews="all"은 선택된 모든 아이콘의 클론을 만듭니다.

폴더로

이 창에서는 선택 사각형으로 파일을 선택하고, 폴더가 파일을 받습니다. 오버레이는 기본 오버레이, 즉 고른 파일의 더미입니다. 폴더에 드롭하면 파일이 하나씩 차례로 들어가고 집은 파일이 마지막에 들어가며, 폴더가 꿀꺽 삼킵니다. 취소하면 각 파일이 자기 파일로 돌아갑니다. 폴더를 클릭하면 열리고, 경로의 Home으로 파일을 다시 드래그할 수 있습니다.

Live

데모 불러오는 중…

<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>
index.vue
<!-- Every picked file gets a clone at pickup: they all go in. -->
<DnDProvider overlay-previews="all">
  <Finder />
</DnDProvider>
FolderTile.vue
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++;
  },
});

이것이 전부입니다. 파일은 드롭과 함께 창에서 사라지므로 폴더 말고는 안착할 곳이 없고, 기본 드롭 애니메이션은 멀티 드래그를 영역 안으로 한 항목씩 차례로 보냅니다. overlayPreviews는 집을 때 클론을 받는 항목 수를 정합니다(기본값 5). 모든 클론이 날아갑니다. 영역 안으로, 자기 자리로, 또는 자기 항목으로 돌아갑니다.

폴더는 버튼입니다. 사각형은 버튼과 입력 필드에서는 시작하지 않으므로, 클릭하면 폴더가 열립니다.

나머지 항목의 움직임: multiple

멀티 드래그가 시작된 리스트가, 드래그하는 동안 선택된 나머지 항목이 어떻게 움직일지 정합니다. 이 페이지의 첫 데모에서 바꿔 보세요.

makeSortableList(listRef, { items: files, multiple: 'gaps' });
multiple드래그하는 동안오버레이
'group'(기본값)나머지 항목이 자리를 떠나 집은 항목과 함께 하나의 자리 블록으로 이동하므로, 리스트는 이미 드롭 결과를 보여 줍니다.리스트 방향으로 늘어선, 드래그 중인 항목의 열(최대 overlayPreviews개).
'gaps'나머지는 드롭할 때까지 data-dnd-dragging으로 표시된 채 제자리에 남습니다.같은 열.
'stack'나머지는 오버레이 안으로 날아갑니다.개수와 함께 표시된 집은 항목.

드롭하면 항목들은 원래 순서대로 대상 인덱스에 함께 안착합니다. 'group'과 'gaps'에서는 오버레이의 각 항목이 자기 자리로 날아갑니다. 이 옵션은 드래그가 시작될 때 읽으므로, ref나 getter로 바꿀 수 있습니다.

리스트가 아닌 드롭 대상(휴지통이나 플레이리스트 등) 위, 또는 드래그를 거부하는 리스트 위에서는 'group' 블록이 흩어집니다. 그곳에 드롭하면 그렇게 되듯 각 항목이 다시 자기 자리에 보이고, 리스트 위로 오면 블록이 다시 만들어집니다.

'gaps'에서 남겨진 항목은 기본적으로 흐려집니다. 원하는 대로 스타일링하세요.

.file[data-dnd-dragging] {
  opacity: 0.35;
}

기본 오버레이

'group'과 'gaps'에서 오버레이는 드래그 중인 항목이 안착할 모습대로 늘어선 열입니다. 집은 항목이 포인터 아래에 있고, 나머지는 드롭 순서대로 그 앞뒤에 놓입니다(가로 리스트에서는 행). 프로바이더의 overlayPreviews만큼 보여 줍니다(기본값 5, 'all'이면 렌더링된 항목 전부). 그보다 많은 항목은 그 자리에서 셉니다. 보이는 항목보다 앞에 안착하는 항목은 열의 시작에 +N, 뒤에 안착하는 항목은 끝에 +N으로 표시됩니다.

그리드에서는 셀 전체로 된 열이 페이지를 덮어 버리므로, 오버레이는 더미 모양입니다. 집은 항목 뒤로 최대 세 개 항목이 부채꼴로 펼쳐지고, 전체 개수가 맨 위에 표시됩니다. 데모를 Grid로 바꿔 보세요.

일반 draggable(makeDraggable)도 위의 파일처럼 더미를 만듭니다. 오버레이의 각 클론은 각자 안착합니다. 리스트에 드롭하면 자기 자리로, 드롭이 항목을 남겨 둔 곳에 있는 자기 항목으로(취소하면 원래 자리로), 그리고 항목이 페이지에서 사라지면 드롭이 들어가는 영역으로 하나씩 차례로 들어가며 박스는 마지막에 들어갑니다.

일부만 받기

리스트의 onBeforeDrop은 멀티 드래그의 일부만 받을 수 있습니다. 받을 key를 반환하면 나머지는 원래 있던 곳으로 돌아갑니다. onBeforeDrop은 비동기일 수 있고, 사용자에게 물어볼 수도 있습니다. 답이 올 때까지 오버레이는 놓은 자리에서 기다립니다. 여기서 Scheduled는 커버가 있는 게시물을 원합니다. 커버가 없는 게시물이 섞인 채 드롭하면, 빠진 커버를 추가해 모두 받을지, 준비된 게시물만 받을지, 아무것도 받지 않을지 묻습니다.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Blog />
  </DnDProvider>
</template>
PostList.vue
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.keys는 드래그 중인 key이고, event.items[i]는 event.keys[i]의 항목입니다.
  • 답이 나온 뒤 onDrop은 accepted와 rejected를 받고, event.result에는 부분 드롭이 남긴 리스트가 들어 있습니다.
  • 드롭이 기다리는 동안 리스트는 드래그 중인 모든 게시물을 안착할 자리에 계속 보여 줍니다. 결정은 답이 합니다. 물어볼 필요가 없는 규칙이라면 group.put이 위에 있는 동안 결정합니다(아래 섞인 타입 참고). 자세한 내용은 드롭 결정하기에 있습니다.

한도

이 보드에서는 In progress와 Review에 카드 수 한도가 있습니다. 꽉 찬 컬럼은 아무것도 받지 않습니다. 그 컬럼의 group.put이 거부하고, 그 위에서는 더미가 빨갛게 변합니다. 고른 카드 중 일부를 받을 자리가 있는 컬럼은 들어가는 만큼만 받고, 나머지는 원래 자리로 돌아갑니다. 그 컬럼의 오버레이가 드롭 전에 이를 알려 줍니다.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Board />
  </DnDProvider>
</template>
WipColumn.vue
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은 함수이며, Vue는 함수를 함수형 컴포넌트로 받아들입니다. fit은 컬럼만 아는 정보인 남은 자리를 FitBadge에 넘기고, 더미는 배지의 슬롯으로 그대로 넘겨 줍니다.

const fit: FunctionalComponent<OverlayProps> = (overlay, { slots }) =>
  h(FitBadge, { count: overlay.items.length, room: room(overlay.items.map((item) => item.id)), limit }, slots);

섞인 타입

리스트는 드롭 전에 타입으로 일부 항목을 거부할 수도 있습니다. acceptMode: 'some'이면 group.put을 항목마다 따로 묻습니다. 하나라도 맞으면 리스트가 드래그를 받고, 나머지는 드롭할 때 원래 자리로 돌아갑니다. 'all'(기본값)이면 모든 항목이 맞아야 합니다.

makeSortableList(gallery, {
  items: photos,
  group: { name: 'files', put: ({ types }) => types.includes('image') },
  acceptMode: 'some',
});

일반 드롭 대상도 acceptMode를 받습니다. 항목을 공유하는 리스트와 드래그 앤 드롭을 참고하세요.

여러 항목을 위한 커스텀 오버레이

오버레이 컴포넌트와 프로바이더의 #overlay 슬롯은 items, 즉 드래그 중인 모든 항목을 받으며, 집은 항목이 처음입니다. 항목 자체 대신 요약을 보여 주세요. 여기서는 개수, 첫 번째 제목, 드롭했을 때 일어날 일입니다.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Mailbox />
  </DnDProvider>
</template>
MailOverlay.vue
<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>
Mailbox.vue
// 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();
  },
});

실제 항목을 배치하려면 위의 바탕화면처럼 DragPreview로 클론을 렌더링하세요. 오버레이 가이드에서는 책 더미를 부채꼴로 펼칩니다. 클론은 집을 때 집은 항목과, 드롭 순서상 그 옆의 다른 항목들에 대해 overlayPreviews만큼 만들어집니다. 그 밖의 항목에는 DragPreview가 기본 슬롯을 렌더링합니다. 드롭할 때 각 미리보기는 자기 항목 위에 안착합니다.

트리

트리에서는 선택된 노드가 문서 순서대로, 각자의 하위 트리와 함께 한 블록으로 움직입니다. 다른 선택된 노드 안에 있는 선택된 노드는 부모의 하위 트리에 실려 함께 갑니다. 트리를 참고하세요.