문서/가이드

축, 경계, 그리드

드래그를 한 축에 고정하고, 박스 안에 가두고, 그리드나 직접 만든 가이드에 스냅합니다. 항목 하나, 리스트 하나, 또는 페이지의 한 부분 전체에 적용할 수 있습니다.

이 페이지 편집

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

드래그는 한 축에 묶어 두거나, 박스 안에 가두거나, 그리드에 스냅하거나, 직접 만든 규칙으로 휘게 할 수 있습니다. 이런 규칙이 모디파이어입니다. 모디파이어는 매 프레임 드래그의 오프셋을 조정합니다. draggable 하나, 리스트, 또는 페이지의 한 부분에 설정하면 오버레이, 드롭 대상, 드롭 모두가 이를 따릅니다.

축 고정

타임라인의 클립은 자기 트랙을 따라 미끄러집니다. 고정을 끄면 트랙도 바꿀 수 있습니다.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Timeline />
  </DnDProvider>
</template>
TimelineClip.vue
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가 리스트나 트리의 것보다 우선하고, draggable(또는 그 리스트)의 것이 드래그 영역의 것보다 우선합니다.
  • 드래그가 시작될 때 읽으므로, 위처럼 getter로 켜고 끌 수 있습니다.
  • 'y'로 고정한 트리도 여전히 중첩됩니다. 이때 깊이는 행 가운데에 머무르는 방식으로 정해집니다.
restrictToVerticalAxis와 restrictToHorizontalAxis 모디파이어보다 axis를 쓰세요. 하는 일은 같지만, 일반 모디파이어라서 체인 중간에 실행되고, 그 뒤의 모디파이어(두 축을 모두 스냅하는 그리드)가 항목을 다시 축 밖으로 옮길 수 있습니다.

모디파이어

모디파이어는 매 프레임 드래그의 오프셋을 조정합니다. 그 결과가 오버레이를 움직이고 동시에 대상을 찾는 지점이 되므로, 리스트는 고정되거나 경계에 걸리거나 스냅된 위치를 기준으로 정렬하고, 드롭의 position은 박스가 최종적으로 놓인 곳입니다.

모디파이어하는 일
restrictToParent드래그가 시작된 컨테이너 안에 오버레이를 가둡니다. 정렬 가능한 리스트, 또는 일반 draggable의 부모 요소입니다.
restrictToElement(target)요소 안에 오버레이를 가둡니다. 템플릿 ref, getter, 또는 요소 자체.
restrictToWindowEdges창 안에 오버레이를 가둡니다.
restrictToFirstScrollableAncestor드래그 중인 요소의 첫 번째 스크롤 가능한 조상 안에 오버레이를 가둡니다.
snapToGrid(size)오버레이를 그리드에 스냅합니다(아래 참고).
snapCenterToCursor집은 지점의 오프셋을 유지하는 대신 오버레이를 포인터 아래 가운데에 둡니다.
restrictToVerticalAxis세로로만 움직입니다. axis: 'y'를 쓰세요.
restrictToHorizontalAxis가로로만 움직입니다. axis: 'x'를 쓰세요.

modifiers는 프로바이더, 드래그 영역, 리스트와 트리, draggable과 정렬 가능한 항목의 옵션입니다. 프로바이더를 제외한 모두가 ref나 getter도 받으며, 드래그가 시작될 때 읽습니다.

안에 가두기

화상 통화의 내 화면은 화면 안에 머뭅니다. 경계를 바꿔 가며 멀리 밖으로 드래그해 보세요.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <CallScreen />
  </DnDProvider>
</template>
SelfView.vue
const { isDragging } = makeDraggable(el, {
  id: 'self-view',
  modifiers: () => props.modifiers, // [restrictToParent], [restrictToWindowEdges] or []
});
  • restrictToParent는 부모 요소인 화면 안에 붙잡아 둡니다. 정렬 가능한 항목의 "부모"는 리스트가 DOM 어디에 있든 그 리스트입니다.
  • restrictToWindowEdges는 화면은 벗어나게 하지만 창은 벗어나지 못하게 합니다. 화면 밖에 드롭하면 드롭 대상 위가 아니므로 원래 자리로 돌아갑니다.
  • 경계가 없으면 어디로든 갑니다.

경계는 오버레이를 현재 크기 그대로 안에 가둡니다. overlayFit: 'slot'에서 더 넓은 항목의 리스트 위에서 커진 박스는 새 크기로 안에 갇힙니다.

그리드에 스냅

플로차트의 노드는 캔버스 그리드에 스냅됩니다. 셀 크기와 부드러운 이동을 바꿔 보세요.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Flowchart />
  </DnDProvider>
</template>
FlowNode.vue
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'(기본값): 오버레이의 왼쪽 위 모서리가 컨테이너(정렬 가능한 리스트, 또는 일반 draggable의 부모)의 왼쪽 위에서 그린 그리드 선에 안착합니다. 'delta': 오프셋 자체를 반올림하므로, 그리드 밖에서 시작한 항목은 계속 그리드 밖에 있으며, 원래 위치에서 셀 단위로 움직입니다. 아무 곳에나 놓이면서(자유 캔버스) 각자의 정렬을 유지해야 하는 항목에 쓰세요.
smoothtrue 또는 { duration, easing }: 오버레이가 셀 사이를 부드럽게 이동합니다(기본 80 ms). 대상은 스냅된 위치를 즉시 씁니다.

origin: 'container'에서는 그리드를 컨테이너의 border box부터 그리세요. 위 캔버스의 테두리는 border가 아니라 box-shadow이므로, 그리드 선이 가장자리에서 바로 시작합니다.

직접 만든 모디파이어

슬라이드의 박스는 다른 박스에 가까워지면 그 가장자리와 중심으로 달라붙고, 분홍색 가이드가 붙은 선을 보여 줍니다.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Slide />
  </DnDProvider>
</template>
Slide.vue
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지금까지의 오프셋.
overlayRecttransform만큼 옮긴 오버레이 박스. 지금의 크기 그대로입니다.
activeRect드래그가 시작될 때의 드래그 중인 요소.
containerRect드래그가 시작된 컨테이너(정렬 가능한 리스트, 또는 부모 요소)의 현재 위치. 스크롤을 따라갑니다.
windowRect창.
origin드래그가 시작될 때의 포인터.
active, over드래그와 그 아래의 대상.

모디파이어는 매 프레임 실행되므로 빠르고 부수 효과가 없게 하세요. 위의 가이드는 모니터의 onDragMove에서 드래그의 delta, 즉 모든 모디파이어를 적용한 뒤의 오프셋으로 그립니다. 타입은 모디파이어에 있습니다.

순서

여러 곳의 모디파이어가 차례로 실행되며, 각각은 앞의 결과에서 출발합니다.

  1. 프로바이더의 modifiers
  2. 드래그가 시작된 드래그 영역의 modifiers
  3. 드래그 중인 항목 자체의 modifiers, 없으면 그 리스트나 트리의 것
  4. 드래그 영역의 restrictToArea
  5. axis 고정

배열 하나 안에서는 순서대로 실행되므로, 스냅과 자석은 경계보다 앞에 두세요. 경계를 마지막에 두면 다른 것이 무엇을 했든 박스를 안에 가둡니다. 위 슬라이드는 [magnet, restrictToParent]를 실행합니다. 순서를 반대로 하면 자석이 슬라이드 가장자리에 붙잡힌 박스를 몇 픽셀 밖으로 끌어낼 수 있습니다.

항목의 modifiers는 리스트의 것을 대체합니다. 더해지지 않습니다. 영역의 경계는 1–3단계의 스냅 다음에 오므로 스냅된 항목도 안에 머물고, 축 고정은 가장 마지막에 옵니다.

경계가 있는 리스트도 양 끝에 닿습니다

레이어 패널입니다. 행은 위아래로만 움직이고 패널을 벗어나지 않습니다. 맨 위 행을 맨 아래까지 드래그해 보세요.

Live

데모 불러오는 중…

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

<template>
  <DnDProvider>
    <Layers />
  </DnDProvider>
</template>
Layers.vue
const { items } = makeSortableList(list, { items: layers, axis: 'y', modifiers: [restrictToParent] });

포인터가 계속 움직여도 경계는 오버레이를 박스 가장자리에 붙잡아 둡니다. 포인터가 리스트 축을 따라 오버레이를 벗어나면 리스트는 대신 포인터를 기준으로 정렬하므로, 항목을 어떻게 잡든 첫 자리와 마지막 자리에 닿을 수 있습니다. 그리드는 계속 오버레이를 기준으로 정렬합니다. 가장자리에 붙잡힌 오버레이가 이미 가장자리 셀을 덮기 때문입니다.

페이지 일부를 위한 규칙

두 영역, 두 규칙입니다. Layout의 스티커는 그리드에 스냅되고 밖으로 나갈 수 없습니다. Sketch의 스티커는 Layout까지 어디로든 갑니다. 드래그는 시작된 영역의 규칙을 유지하며, 규칙이 적용 중인 영역이 강조됩니다.

Live

데모 불러오는 중…

<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>
DragArea.vue
<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는 요소 안에서 시작하는 드래그의 규칙을 정합니다. 일반 draggable이든 정렬 가능한 항목이든 마찬가지이며, 그러면 항목 자체에는 규칙이 필요 없습니다. 앱 전체에는 DnDProvider를 하나만 두고, 보드, 캔버스, 피드에는 각자의 영역을 주세요.

옵션하는 일
axis드래그를 한 축에 고정합니다. draggable 자체의 axis나 그 리스트의 것이 우선합니다.
modifiers프로바이더의 것 다음, draggable 자체의 것 전에 실행됩니다.
restrictToAreatrue: 드래그가 영역의 요소 안에 머뭅니다.
autoScroll영역의 스크롤 컨테이너(요소, 또는 가장 가까운 스크롤 가능한 조상)의 자동 스크롤. false면 그곳에서 끕니다.
activation포인터 종류별 활성화. 스크롤되는 피드에서 터치를 더 길게 누르게 하는 식입니다. draggable 자체의 것이 우선하고, 프로바이더의 것이 마지막입니다.
disabled영역 안에서도, 그 안의 영역에서도 드래그가 시작되지 않습니다. 영역의 draggable은 aria-disabled를 알립니다.

모든 옵션은 값, ref, getter를 받으며, 누르기나 드래그가 시작될 때 읽습니다. 영역은 isDragging(여기서 시작한 드래그가 진행 중. 위의 강조된 테두리가 이를 보여 줍니다)과 isAutoScrolling(자동 스크롤이 컨테이너를 움직이는 중)을 반환합니다. 전체 레퍼런스는 makeDragArea에 있습니다.

영역의 결합

  • 드래그는 영역을 떠난 뒤에도 시작된 영역을 따릅니다. 영역 위를 지나가도 아무것도 바뀌지 않습니다. 단, autoScroll은 예외로, 그 컨테이너를 스크롤하는 모든 드래그에 적용됩니다.
  • 중첩된 영역은 합쳐지지 않습니다. 가장 가까운 영역이 우선합니다. disabled는 예외로 상속됩니다. 꺼진 영역은 그 안의 영역도 끕니다.
  • 충돌 감지, 키보드 키, 안내 문구는 프로바이더 전체에 적용됩니다.
보드를 두 번째 DnDProvider로 감싸면 어떨까요? 프로바이더는 서로 독립적입니다. 드래그는 한 프로바이더에서 다른 프로바이더로 넘어가지 않으므로, 보드의 항목은 보드를 벗어날 수 없게 됩니다. 프로바이더는 페이지의 한 부분이 프로바이더 전체에 걸친 설정을 따로 가져야 하고 그 항목이 그 안에 머물 때만 중첩하세요. 프로바이더를 참고하세요.

놓은 자리에 그대로 두기

위의 타임라인, 통화 화면, 플로차트, 슬라이드, 영역은 모두 각 항목을 놓은 자리에 둡니다. 모든 드래그 이벤트에는 position이 들어 있습니다. 대상 안에서 드래그 중인 박스의 왼쪽 위 모서리로, 대상의 패딩 가장자리부터 재고 스크롤 양을 더한 값입니다. 절대 위치로 배치한 항목을 바로 그 자리에 두는 left와 top입니다. 모디파이어 다음에 계산되므로 스냅된 노드는 그리드에 안착합니다.

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

자유로운 드래그와 드롭 대상에 대해서는 드래그 앤 드롭을 참고하세요.