문서/가이드

프로바이더

DnDProvider는 그 아래의 모든 드래그를 관리합니다. 대상을 찾고, 드래그를 시작하고, 스크롤하며, 일어나는 모든 일을 useDnD와 useDnDMonitor에 알립니다.

이 페이지 편집

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

앱에 프로바이더 하나

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

<template>
  <DnDProvider>
    <RouterView />
  </DnDProvider>
</template>

DnDProvider 하나를 App.vue나 레이아웃처럼 높은 곳에 두고 신경 쓰지 마세요. 모든 컴포저블(makeSortableList, makeDraggable, useDnD…)은 자신이 실행되는 컴포넌트 위에서 가장 가까운 프로바이더를 찾습니다.

컴포저블은 프로바이더 안에서 렌더링되는 컴포넌트에서 실행해야 합니다. 자기 템플릿에 <DnDProvider>가 들어 있는 컴포넌트에서 호출하면 No <DnDProvider> found above this component 오류가 발생합니다. Vue의 inject는 조상만 볼 수 있기 때문입니다.

프로바이더가 관리하는 것

부분프로바이더가 하는 일
입력포인터, 터치, 키보드 입력을 드래그로 바꿉니다(sensors, activation). 드래그는 한 번에 하나입니다.
대상 찾기드래그 아래에 있는 드롭 대상을 히트 테스트하고(collision) 대상에게 알립니다.
오버레이포인터를 따라가는 박스를 body로 텔레포트해 렌더링하고, 드롭하면 안착시킵니다.
선택선택된 id의 집합 하나를 그 아래의 모든 리스트와 draggable이 공유합니다.
자동 스크롤드래그가 컨테이너나 창의 가장자리에 다가가면 스크롤합니다.
라이브 리전집기, 이동, 드롭, 취소를 스크린 리더에 안내합니다.

그 밖에는 아무것도 렌더링하지 않습니다. 마크업은 그대로 유지됩니다.

옵션 한눈에 보기

모든 옵션에는 잘 동작하는 기본값이 있어서, 보통 프로바이더에는 옵션을 넘기지 않습니다. 프로바이더는 필요할 때 props를 읽습니다. 아래 데모에서는 collision, activation, autoScroll을 실시간으로 바꿉니다.

Prop기본값설정하는 것
collisionpointerFirst드래그 아래의 대상을 고르는 방식. 아래 참고.
activation마우스 4 px, 터치 200 ms누르기가 드래그로 바뀌는 시점(포인터 종류별). 아래 참고.
autoScroll켜짐가장자리 스크롤: { threshold, maxSpeed, acceleration } 또는 false. 아래 참고.
haptics터치로 집을 때 짧은 진동터치 드래그의 진동, 또는 false. 아래 참고.
copyModifier'Alt'드래그하는 동안 누르고 있으면 이동을 복사로 바꾸는 키.
modifiers[]모든 드래그에 적용하는 이동 규칙. 제약을 참고하세요.
animation / dropAnimation켜짐정렬 중 항목이 미끄러지는 애니메이션 / 오버레이의 안착. 애니메이션을 참고하세요.
animateChanges꺼짐리스트와 트리의 기본값: 드래그가 아닌 데이터 변경을 애니메이션합니다.
keyboardSpace / Enter, Escape키보드 키와 이동 단위. false면 키보드 드래그를 끕니다. 접근성을 참고하세요.
sensors포인터 + 키보드입력을 드래그로 바꾸는 것. 센서를 참고하세요.
overlayTo'body'오버레이를 텔레포트할 위치.
overlayFit'slot'오버레이 크기: 안착할 자리의 크기 또는 원본의 크기. 오버레이를 참고하세요.
overlayTransition크로스페이드드래그 중에 오버레이가 한 컴포넌트에서 다른 컴포넌트로 바뀌는 방식: false 또는 <Transition> props. 오버레이를 바꾸는 대상을 참고하세요.
overlayClass—오버레이 콘텐츠([data-dnd-overlay-content])에 붙는 클래스. 통째로 애니메이션할 요소입니다. 드래그로 집을 때는 들어 올리고, 안착할 때는 내려놓습니다. 오버레이: 들어 올리고, 기울이고, 내려놓기를 참고하세요.
overlayPreviews5멀티 드래그: 집을 때 클론을 받는 드래그 항목의 수, 또는 'all'. 기본 오버레이를 참고하세요.
announcements기본 문구스크린 리더 문구, 또는 false.
screenReaderInstructions기본 문구모든 draggable이 aria-describedby로 가리키는 텍스트.
defaultStylestrue드래그 상태용 명시도 0의 CSS를 주입합니다. 스타일링을 참고하세요.

모든 형태가 담긴 전체 목록은 DnDProvider API에 있습니다. 프로바이더는 drag-start, drag-move, drag-over, drag-end, drag-cancel, drag-resume, drag-settle 이벤트도 내보냅니다. useDnDMonitor가 듣는 것과 같은 이벤트입니다.

드래그 읽기: useDnD()

Live

데모 불러오는 중…

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

<template>
  <!-- One provider: the list drags, the panel reads the drag. Neither imports the other. -->
  <DnDProvider>
    <div class="demo">
      <Tasks />
      <StatePanel />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 24px;
}
</style>

useDnD()는 프로바이더 아래의 어느 컴포넌트에서든 드래그를 반응형 상태로 반환합니다.

import { useDnD } from '@vue-dnd-kit/core';

const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
필드담고 있는 것
status'idle', 'activating'(활성화를 기다리는 누르기), 'dragging', 'dropping'(진행 중인 드래그는 없지만 오버레이가 아직 안착 중이거나, 비동기 onBeforeDrop이 결정하는 중), 'selecting'(선택 사각형).
active드래그 중인 것: id, ids(드래그 중인 모든 id, 집은 항목이 처음), items, data, type, element, rect, sensor. 대기 중에는 null.
over드래그 아래의 대상: id, data, element, rect, accepted. 아무것도 없으면 null.
pointer모디파이어를 적용한 뒤의 뷰포트 좌표상 포인터.
activation.progress누르기가 활성화를 기다리는 동안 0에서 1까지. 링을 그릴 때 쓰세요.
isAutoScrolling자동 스크롤이 무언가를 움직이고 있습니다.
selection프로바이더의 선택: ids, has, add, delete, toggle, set, clear. 멀티 드래그를 참고하세요.
cancel()진행 중인 드래그(항목이 원래 자리로 돌아갑니다), 충전 중인 누르기, 선택 사각형을 취소합니다.
DropHint.vue
<script setup lang="ts">
import { computed } from 'vue';
import { useDnD } from '@vue-dnd-kit/core';

const { status, over } = useDnD();
const refused = computed(() => status.value === 'dragging' && over.value?.accepted === false);
</script>

<template>
  <p v-if="refused" role="status">This list doesn't take that.</p>
</template>

드롭이 결정되는 즉시 active, over, pointer는 null이 됩니다. 오버레이가 아직 안착 중이어도 마찬가지입니다(status는 'dropping'입니다). 그래서 이 값들에서 파생된 UI가 새 데이터 옆에 이전 드래그를 보여 주는 일은 없습니다. 비동기 onBeforeDrop이 기다리는 동안에는 놓은 순간의 상태가 그대로 유지됩니다.

다음 드래그는 안착을 기다리지 않습니다. 바로 시작되고, 직전 오버레이가 그 주위에서 안착하는 동안 status는 'dragging'입니다. 안착 중인 오버레이를 누르면 날아가는 도중에 붙잡습니다. 드래그는 오버레이가 있는 자리에서 이어집니다. 드래그의 오버레이가 제자리에 도착한 시점은 onDragSettle이 알려 줍니다.

모든 드래그 듣기: useDnDMonitor()

import { useDnDMonitor } from '@vue-dnd-kit/core';

useDnDMonitor({
  onDragStart: ({ active }) => {},
  onDragMove: ({ pointer, delta }) => {},
  onDragOver: ({ over }) => {},                  // the target changed
  onBeforeDrop: (e) => 'accept',                 // or 'reject', 'continue', some ids; may be async
  onDragEnd: ({ active, over, dropped, accepted, position }) => {},
  onDragCancel: ({ active }) => {},
  onDragResume: ({ active }) => {},              // onBeforeDrop answered 'continue'
  onDragSettle: ({ active, over }) => {},        // its overlay has landed
});

모니터는 어디서 시작했든 자기 프로바이더 아래의 모든 드래그를 듣습니다. 위 패널은 리스트를 import하지 않고도 리스트의 드래그를 기록합니다. 모니터는 자신을 호출한 컴포넌트가 언마운트되면 멈춥니다.

이벤트시점
onDragStart드래그가 시작됩니다.
onDragMove드래그가 움직입니다.
onDragOver드래그 아래의 대상이 바뀝니다.
onBeforeDrop드래그를 놓습니다. 반환값이 드롭을 결정합니다. 드롭 결정하기를 참고하세요.
onDragEnd드롭이 완료되었습니다. 정렬 가능한 리스트는 이미 onDrop을 실행하고 새 순서를 기록했습니다. 대상이 없는 곳에서 끝나면 dropped는 false입니다.
onDragCancelEscape, 드롭을 거부하는 대상 위의 드롭, 또는 'reject'로 답한 onBeforeDrop.
onDragResumeonBeforeDrop이 'continue'로 답했습니다. 아무것도 드롭되지 않고 드래그가 계속됩니다.
onDragSettle드래그의 오버레이가 안착했습니다(또는 원래 자리로 돌아갔거나, 다음 드래그가 붙잡았습니다). onDragEnd나 onDragCancel이 받은 이벤트가 함께 전달됩니다. 그사이 다른 드래그가 시작되었을 수 있습니다.

모든 이벤트에는 active, over, pointer, delta(시작점으로부터의 오프셋), position(드래그 중인 박스가 over 안에서 놓인 위치, 드래그 앤 드롭 참고)이 들어 있고, 정렬 가능한 리스트의 항목이라면 source도 들어 있습니다. 전체 형태는 이벤트 API에 있습니다.

저장, 선택 해제, 분석처럼 "드롭 후"에 할 일은 onDragEnd에서 하세요. 리스트 자체의 onDrop은 기록 전에 실행되므로 아직 이전 순서를 봅니다.

대상 찾기

함수를 고른 뒤 핀이나 카드를 대상 위로 드래그해 보세요. 배지는 이 프레임에서 함수가 대상의 순위를 어떻게 매기는지 보여 주며, 초록색이 선택된 대상입니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import {
  DnDProvider,
  closestCenter,
  closestCorners,
  pointerFirst,
  pointerWithin,
  rectIntersection,
  type CollisionDetection,
  type DnDEndEvent,
  type Key,
} from '@vue-dnd-kit/core';
import Desk from './Desk.vue';
import { magnet } from './magnet';

const DETECTORS: Record<string, CollisionDetection> = {
  pointerFirst,
  pointerWithin,
  rectIntersection,
  closestCenter,
  closestCorners,
  magnet,
};
const names = Object.keys(DETECTORS);
const mode = ref('pointerFirst');

// The ranking of the current frame, best first: a wrapper around the chosen function
// records it, so the targets can show their rank.
const ranked = shallowRef<Key[]>([]);
const collision = computed<CollisionDetection>(() => {
  const detect = DETECTORS[mode.value]!;
  return (args) => (ranked.value = detect(args));
});

const result = ref('Drag the pin or the card over the targets');
const onEnd = ({ over }: DnDEndEvent) => {
  ranked.value = [];
  result.value = over ? `Dropped on ${over.id}` : 'Dropped on nothing';
};
const onCancel = () => {
  ranked.value = [];
  result.value = 'Cancelled';
};
</script>

<template>
  <DnDProvider :collision="collision" @drag-end="onEnd" @drag-cancel="onCancel">
    <div class="lab">
      <div class="modes" role="radiogroup" aria-label="Collision detection">
        <label v-for="name in names" :key="name" :class="{ on: mode === name }">
          <input v-model="mode" type="radio" :value="name" />
          {{ name }}
        </label>
      </div>
      <code class="call">&lt;DnDProvider :collision="{{ mode }}"&gt;</code>
      <Desk :ranked="ranked" />
      <p class="result" aria-live="polite">{{ result }}</p>
    </div>
  </DnDProvider>
</template>

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

.modes {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px;
  max-width: 100%;
  padding: 3px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

.modes label {
  padding: 5px 9px;
  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;
}

.result {
  margin: 0;
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.6;
}
</style>

드래그의 매 프레임마다 프로바이더는 관련된 드롭 대상을 collision에 넘기고, 함수는 그 id를 좋은 순서대로 반환합니다. 라이브러리에는 다섯 가지 함수가 들어 있습니다.

함수순위를 매기는 대상어울리는 경우
pointerFirst포인터 아래에 있는 것. 아무것도 없으면 드래그 중인 박스와 겹치는 것.기본값: 포인터 아래에서는 정확하고, 빈틈에서는 너그럽습니다.
pointerWithin포인터 아래에 있는 것만, 가장 안쪽부터.정확한 대상: 빈틈에서는 어디에도 드롭되지 않습니다.
rectIntersection드래그 중인 박스와 겹치는 모든 대상: priority가 높은 순, 그다음 더 깊이 중첩된 순, 그다음 더 많이 겹친 순.작은 대상 위의 큰 항목, 포인터가 항목 가장자리에 있을 때.
closestCenter모든 대상, 박스 중심에서 대상 중심까지의 거리순.멀리 있어도 항상 무언가가 선택됩니다. 같은 크기 항목의 그리드와 리스트.
closestCorners모든 대상, 네 모서리 사이의 거리순.나란히 놓인 컬럼(칸반), 높이가 크게 다른 대상.

포인터 아래에서 히트 테스트는 사용자가 보는 그대로를 따릅니다. 겹친 대상 중에서는 맨 위의 것만 맞고(겹친 부분에서는 back이 아니라 항상 front), 중첩된 대상은 가장 안쪽이 먼저입니다(subfolder, 그다음 folder).

직접 만든 함수

magnet.ts
import type { CollisionDetection, Coordinates, Rect } from '@vue-dnd-kit/core';

/** How far the point is from the rect: 0 inside it. */
const gap = (rect: Rect, point: Coordinates) =>
  Math.hypot(
    Math.max(rect.left - point.x, 0, point.x - rect.right),
    Math.max(rect.top - point.y, 0, point.y - rect.bottom),
  );

/** What is under the pointer; in a gap, the nearest target within 40px. */
export const magnet: CollisionDetection = ({ hits, pointer, candidates }) => {
  if (hits.length) return hits;
  return candidates
    .filter((candidate) => gap(candidate.rect, pointer) <= 40)
    .sort((a, b) => gap(a.rect, pointer) - gap(b.rect, pointer))
    .map((candidate) => candidate.id);
};
인자담고 있는 것
pointer모디파이어를 적용한 뒤의 포인터.
overlayRect화면에 보이는 그대로의 드래그 중인 박스. 오버레이가 없으면 null.
candidates관련된 모든 대상: id, rect(스크롤 컨테이너에 잘린, 보이는 부분), priority, depth(최상위는 0, 중첩될수록 커짐).
hits히트 테스트로 찾은 포인터 아래의 대상, 좋은 순서대로.

"대상 없음"은 빈 배열을 반환하세요. 비활성화된 대상, 드래그 중인 항목 안의 대상, 스크롤 컨테이너에 가려진 대상은 함수에 전달되지 않습니다. 드래그를 거부하는 대상도 경쟁에는 참여합니다. 그 위에서는 over.accepted가 false이고 드롭은 거부됩니다(정렬 가능한 항목 안의 대상은 대신 비켜서므로, 리스트는 그 위에서 정렬됩니다). 데모는 고른 함수를 감싸 순위를 기록합니다. (args) => (ranked.value = detect(args)).

프로바이더는 함수가 요청하는 대상 전환을 완화합니다. 드래그가 방금 떠난 대상은 포인터가 10 px 움직이기 전까지 다시 선택될 수 없으므로 경계에서 깜박이지 않습니다. 한 부모 안에 중첩된 대상 사이(카드 컬럼의 빈틈)에서는 마지막 대상이 유지됩니다.

누르기가 드래그로 바뀌는 시점

누르자마자 드래그되지는 않습니다. activation은 포인터 종류(mouse, touch, pen)마다 무엇을 기다릴지 정합니다. px 단위의 distance, ms 단위의 delay(이를 넘으면 누르기를 포기하는 tolerance와 함께 써서 페이지가 스크롤될 수 있게 합니다), 또는 둘 중 먼저 오는 것입니다.

<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
포인터기본값
mouse, pen{ distance: 4 }: 클릭은 클릭으로 남습니다. handle을 마우스로 누르면 바로 시작하며, 움직이지 않으면 여전히 클릭입니다.
touch{ delay: 200, tolerance: 5 }: 길게 누르기. 손가락 아래에서 리스트가 계속 스크롤됩니다.

지정하지 않은 종류는 기본값을 유지합니다. draggable 자체의 activation이 드래그 영역의 것보다 우선하고, 드래그 영역의 것이 프로바이더의 것보다 우선합니다. 누르기가 기다리는 동안 useDnD().status는 'activating'이고 activation.progress는 0에서 1까지 진행합니다. Escape나 useDnD().cancel()로 포기할 수 있습니다. 조건을 실시간으로 시험해 보고 진행률로 길게 누르기 링을 그리는 방법은 센서에 있습니다.

자동 스크롤

채널을 리스트의 위나 아래 쪽으로 드래그해 보세요. 초록색 띠가 가장자리 영역입니다. 띠 안으로 깊이 들어갈수록 리스트가 빠르게 스크롤됩니다.

Live

데모 불러오는 중…

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

const OPTIONS = {
  threshold: [0.1, 0.2, 0.4],
  maxSpeed: [10, 25, 50],
  acceleration: [4, 10, 25],
} as const;

const on = ref(true);
// The defaults: an edge zone of 20% of the container (24 to 96px), 10px a frame at the
// edge, up to 25px a frame past it.
const options = ref({ threshold: 0.2, maxSpeed: 25, acceleration: 10 });
const autoScroll = computed(() => (on.value ? options.value : false));
const call = computed(() =>
  on.value
    ? `:auto-scroll="{ threshold: ${options.value.threshold}, maxSpeed: ${options.value.maxSpeed}, acceleration: ${options.value.acceleration} }"`
    : ':auto-scroll="false"',
);
</script>

<template>
  <DnDProvider :auto-scroll="autoScroll">
    <div class="demo">
      <div class="switches">
        <label class="switch">
          <input v-model="on" type="checkbox" />
          autoScroll
        </label>
        <fieldset v-for="(values, name) in OPTIONS" :key="name" :disabled="!on">
          <legend>{{ name }}</legend>
          <label v-for="value in values" :key="value" :class="{ on: options[name] === value }">
            <input v-model="options[name]" type="radio" :value="value" />
            {{ value }}
          </label>
        </fieldset>
      </div>
      <code class="call">{{ call }}</code>
      <Channels :threshold="on ? options.threshold : 0" />
    </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 14px;
}

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

fieldset:disabled {
  opacity: 0.4;
}

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

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

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

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

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

.switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.switch input {
  accent-color: #42b883;
}

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

overflow: auto나 scroll인 요소는 모두 저절로 스크롤되고, 창도 마찬가지입니다. 프로바이더는 포인터 아래의 컨테이너를 먼저 스크롤하고, 그다음 그 조상, 그다음 창을 스크롤합니다.

옵션기본값설정하는 것
threshold0.2가장자리 영역. 컨테이너 크기에 대한 비율이며 24~96 px 사이입니다.
acceleration10가장자리 끝에서의 속도(프레임당 px). 영역 안으로 들어간 깊이의 제곱에 비례해 커지므로, 처음에는 느리고 정밀합니다.
maxSpeed25최고 속도. 가장자리를 넘어가면(포인터가 컨테이너 밖에 있으면) 속도가 여기까지 계속 커집니다.

:auto-scroll="false"로 끕니다. 포인터가 그 방향으로 움직인 뒤에야 그 방향으로 스크롤되므로, 가장자리 근처에서 집은 항목이 저절로 스크롤되지는 않습니다. 리스트, 트리, 드래그 영역은 자기 컨테이너를 위한 autoScroll을 따로 받습니다.

makeSortableList(list, { items: channels, autoScroll: { maxSpeed: 10 } });

키보드 드래그는 자동 스크롤하지 않습니다. 대신 항목이 보이도록 스크롤합니다. isAutoScrolling(useDnD()나 드래그 영역에서 얻음)은 무언가가 스크롤되는 동안 true입니다.

진동과 복사 키

터치 드래그는 시작할 때 짧게 진동합니다. :haptics="false"로 끄고, 객체를 넘기면 진동을 하나씩 설정할 수 있습니다.

<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
키진동하는 시점기본값
start길게 누르기가 드래그로 바뀔 때.true(10 ms)
enter드래그가 자신을 받아들이는 대상에 들어갈 때.꺼짐
drop드롭이 완료될 때.꺼짐
refuse드롭이 거부되어 항목이 원래 자리로 돌아갈 때.꺼짐

각각 true(기본 패턴), ms 단위 길이, 켜짐/꺼짐 패턴, false를 받습니다. 터치로 시작한 드래그만 진동하며, iOS Safari는 진동을 지원하지 않습니다.

copyModifier는 리스트 사이를 드래그하는 동안 누르고 있으면 이동을 복사로 바꾸는 키입니다. 기본값은 'Alt'이고, 어떤 KeyboardEvent.key든 쓸 수 있으며, null이면 없습니다. 항목이 출발하는 리스트에는 clone 함수가 필요합니다. 항목을 공유하는 리스트를 참고하세요.

여러 프로바이더

대부분의 앱에는 정확히 하나만 필요합니다. 위시리스트를 별도의 프로바이더 아래에 두고 책을 반대편으로 옮겨 보세요. 책은 자기 프로바이더를 벗어날 수 없고, 바깥 모니터는 더 이상 위시리스트를 듣지 못합니다.

Live

데모 불러오는 중…

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

const reading = ref([
  { id: 'dune', title: 'Dune', hue: 32 },
  { id: 'emma', title: 'Emma', hue: 330 },
  { id: 'ulysses', title: 'Ulysses', hue: 210 },
]);
const wishlist = ref([
  { id: 'beloved', title: 'Beloved', hue: 140 },
  { id: 'solaris', title: 'Solaris', hue: 260 },
]);

const separate = ref(false);
</script>

<template>
  <DnDProvider>
    <div class="demo">
      <div class="modes" role="radiogroup" aria-label="Wishlist">
        <label :class="{ on: !separate }"><input v-model="separate" type="radio" :value="false" />one provider</label>
        <label :class="{ on: separate }"><input v-model="separate" type="radio" :value="true" />wishlist in its own</label>
      </div>
      <div class="columns">
        <div class="column">
          <Shelf id="reading" v-model="reading" title="Reading" />
          <EventLog title="outer monitor" />
        </div>
        <!-- A provider inside a provider: its items belong to it, and drags never cross. -->
        <DnDProvider v-if="separate">
          <div class="column island">
            <span class="tag">&lt;DnDProvider&gt;</span>
            <Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
            <EventLog title="inner monitor" />
          </div>
        </DnDProvider>
        <div v-else class="column">
          <Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
        </div>
      </div>
    </div>
  </DnDProvider>
</template>

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

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

.modes label {
  padding: 5px 10px;
  border-radius: 6px;
  font-size: 12px;
  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;
}

.columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 230px));
  gap: 16px;
  max-width: 100%;
}

.column {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

.island {
  position: relative;
  margin: -10px;
  padding: 10px;
  border: 1.5px dashed color-mix(in srgb, #42b883 60%, transparent);
  border-radius: 16px;
}

.tag {
  position: absolute;
  top: -9px;
  right: 14px;
  padding: 0 6px;
  background: Canvas;
  font: 10px/18px ui-monospace, Menlo, Consolas, monospace;
  color: #42b883;
}
</style>
<DnDProvider>
  <Shelf v-model="reading" />
  <DnDProvider>
    <Shelf v-model="wishlist" />
    <EventLog />            <!-- hears the inner provider's drags only -->
  </DnDProvider>
</DnDProvider>
  • 프로바이더는 서로 독립적입니다. 드래그는 한 프로바이더에서 다른 프로바이더로 넘어가지 않습니다.
  • 프로바이더를 중첩할 수 있습니다. 가장 가까운 소유자가 우선합니다. 항목은 자신 위의 가장 가까운 프로바이더에 속하고, 바깥 프로바이더는 그 항목을 무시합니다.
  • 프로바이더마다 선택, 오버레이, 라이브 리전, 모니터를 따로 가집니다.

두 번째 프로바이더는 프로바이더 전체에 걸친 설정(충돌 감지, 키보드 키, 안내 문구)을 따로 두어야 하고, 항목이 그 밖으로 나가지 않는 페이지 일부에만 의미가 있습니다. 페이지의 두 부분 사이에서 항목이 움직이지 않는다면, 두 프로바이더 아래에 있지 않은지 확인하세요.

페이지 일부에만 적용하는 규칙(스냅되는 보드, 터치를 더 길게 눌러야 하는 피드, 아무것도 드래그되지 않는 패널)에는 별도의 프로바이더가 필요 없습니다.
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
영역의 항목은 앱의 프로바이더에 그대로 남아 있으므로 어디로든 옮길 수 있습니다. 제약을 참고하세요.

다른 곳에 마운트된 앱(섀도 루트 안에 있거나 별도의 Vue 앱인 경우)도 기존 프로바이더에 합류할 수 있습니다. 마운트하기 전에 useDnDContext()를 provideDnDContext(app, context)에 넘기세요. 히트 테스트와 오버레이는 섀도 루트를 넘나들며 동작합니다.

SSR과 Nuxt

프로바이더는 SSR에서 안전합니다. 리스트와 항목은 다른 마크업처럼 서버에서 렌더링됩니다. 오버레이와 라이브 리전은 텔레포트되어 클라이언트에서만 마운트되므로 하이드레이션 불일치를 일으키지 않습니다. 기본 id는 Vue의 useId()에서 오므로 서버와 클라이언트에서 일치합니다.

Nuxt에서는 모듈이 DnDProvider를 전역으로 등록하고 컴포저블을 자동으로 import합니다. 설치를 참고하세요.