본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

DragDrop 2.0 드래그

원문 보기 위키 갱신

컨테이너 사이에서 요소를 자유롭게 끌어다 놓고 정렬할 수 있게 도와주는 드래그 앤 드롭 컴포넌트예요.

출처: 문서

본문

언제 사용하나요?

드래그 앤 드롭을 제공하는 컴포넌트예요.

기본 사용법

:::demo 한 컨테이너에서 다른 컨테이너로 요소를 끌어다 놓고, 정렬도 지원해요.

<script>
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  setup() {
    const data = reactive({
      list1: [
        { name: 'Visual Studio Code' },
        { name: 'WebStorm' },
        { name: 'Sublime' },
        { name: 'Atom (disabled)', disabled: true }
      ],
      list2: [],
      list3: [],
    });

    function onDrop(e, targetArray) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const item = e.dragData.item;
      if (-1 !== index) {
        /* 修正同一个container排序,往下拖动index多了1个位置*/
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        targetArray.splice(index, 0, fromIndex === -1 ? item : targetArray.splice(fromIndex, 1)[0]);
      } else {
        targetArray.push(item);
      }
      if (fromIndex === -1) {
        removeItem(item, e.dragData.parent);
      }
    }

    function removeItem(item, list) {
      const index = list.map(function (e) {
        return e.name;
      }).indexOf(item.name);
      list.splice(index, 1);
    }

    function log(...v) {
      console.log(...v);
    }

    return { log, removeItem, onDrop, data };
  },
});
</script>

<template>
<div class="demo1">
<div class="row">
  <div class="col-sm-3">
    <div class="card">
      <div class="card-header">Draggable Item</div>
      <div class="card-block">
        <ul class="list-group">
          <li
            v-dDraggable="{
              dragScope: 'default-css',
              dragData:{ item: item, parent: data.list1 },
              disabled: item.disabled,
              '@dragStartEvent': log,
              '@dragEndEvent': log,
              '@dragEndEvent': log,
            }"
            v-for="item in data.list1"
            :class="['list-group-item over-flow-ellipsis', { disabled: item.disabled }]"
          >
            {{ item.name }}
          </li>
        </ul>
      </div>
    </div>
  </div>
  <div class="col-sm-3">
    <div
      class="card card-outline-primary mb-3"
      v-dDroppable="{
        dropScope: 'default-css',
        '@dropEvent': function (e) {onDrop(e, data.list2)},
        '@dragEnterEvent': log,
        '@dragOverEvent': log,
        '@dragLeaveEvent': log,
      }"
    >
      <div class="card-header">Drop Area With Sortable Function</div>
      <div class="card-block">
        <ul class="list-group d-drop-el" v-dSortable>
          <li
            v-dDraggable="{
              dragScope:'default-css',
              dragData:{ item: item, parent: data.list2 },
              disabled: item.disabled,
            }"
            v-for="item in data.list2"
            :class="['list-group-item over-flow-ellipsis', { disabled: item.disabled }]"
          >
            {{ item.name }}
          </li>
        </ul>
      </div>
    </div>
  </div>
   <div class="col-sm-3">
    <div
      class="card card-outline-primary mb-3"
      v-dDroppable="{
        dropScope: 'default-css',
        '@dropEvent': function (e) {onDrop(e, data.list3)},
        '@dragEnterEvent': log,
        '@dragOverEvent': log,
        '@dragLeaveEvent': log,
      }"
    >
      <div class="card-header">Drop Area Without Sortable Function</div>
      <div class="card-block">
        <ul class="list-group drop-el">
          <li
            v-dDraggable="{
              dragScope:'default-css',
              dragData:{ item: item, parent: data.list3 },
              disabled: item.disabled,
            }"
            v-for="item in data.list3"
            :class="['list-group-item over-flow-ellipsis', { disabled: item.disabled }]"
          >
            <div>
              {{ item.name }}
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
</div>
</template>
<style lang='scss'>
.demo1 {
  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size);
    padding: 10px;
  }

  .list-group {
    margin-bottom: 20px;
    padding: 10px;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }

  li.list-group-item.disabled {
    border-color: var(--devui-line);
    color: var(--devui-disabled-text);
    background-color: var(--devui-disabled-bg);
  }
}

</style>

:::

다층 트리 드래그

:::demo 정렬할 때 요소 위로 드래그할 수 있고, 계층 중첩을 지원해요.

<script>
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  name: 'demo-item',
  props: {
    parentItem: Object,
  },
  setup() {
    const data = reactive({
      list1: [
        { name: 'Visual Studio Code' },
        { name: 'Sublime' },
        { name: 'Atom' }
      ],
      list2: [
        { name: 'WebStorm' , children: [{ name: 'notepad++'}]},
      ],
    });

    function onDrop(e, list) {
      const target = list;
      const item = e.dragData.item;
      const parent = e.dragData.parentList;
      const indexOfParent = e.dragData.index;
      const dropIndex = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const dropOnItem = e.dropOnItem;

      // 同表排序的情况下index会有变化, 跨表不会有变化
      let index = dropIndex;
      if (target === parent && dropIndex > fromIndex) {
        index--;
      }
      // 源数组移除
      parent.splice(indexOfParent, 1);
      // 插入
      if (dropOnItem) {
        target[index].children = target[index].children || [];
        target[index].children.push(item);
        return;
      }
      if (index < 0) {
        target.push(item);
        return;
      }
      target.splice(index, 0, item);
    }

    return { onDrop, data };
  },
});
</script>
<template>
<div class="demo2" v-if="!parentItem">
  <div class="row">
    <div class="col-sm-3">
      <div class="card">
        <div class="card-header">Draggable Items</div>
        <div class="card-block">
          <ul class="list-group">
            <li
              v-for="(item, i) in data.list1"
              v-dDraggable="{
                dragScope: 'drop-area',
                dragData: { item: item, parentList: data.list1, index: i },
              }"
              class="list-group-item"
            >
              <div class="name">{{ item.name }}</div>
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          dropScope: 'drop-area',
          allowDropOnItem: true,
          nestingTargetRect: { height: 30 },
          dragOverItemClass: 'drag-over-item',
          '@dropEvent': (e) => { onDrop(e, data.list2) },
        }"
      >
        <div class="card-header">Tree Sortable Area</div>
        <div class="card-block">
          <ul class="list-group d-drop-el" v-dSortable>
            <li
              v-for="(item, i) of data.list2"
              v-dDraggable="{
                dragScope: 'drop-area',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parentList: data.list2, index: i },
              }"
            >
              <div class="name">
                {{ item.name }}
              </div>
              <demo-item v-if="item.children && item.children.length > 0" :parentItem="item"></demo-item>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</div>
<ul class="list-group list-group-child" v-if="parentItem"
    v-dSortable 
    v-dDroppable="{
      dropScope: 'drop-area',
      allowDropOnItem: true,
      nestingTargetRect: { height: 30 },
      dragOverItemClass: 'drag-over-item',
      '@dropEvent': (e) => onDrop(e, parentItem.children),
    }" 
  >
    <li 
      v-for="(item, i) of parentItem.children" 
      v-dDraggable="{
        dragHandleClass: 'drag-handle',
        dragScope: 'drop-area',
        dragData:{ item: item, parentList: parentItem.children, index: i },
      }"
    >
      <div class="name">
        {{ item.name }}
      </div>
      <demo-item v-if="item.children && item.children.length > 0" :parentItem="item"></demo-item>
    </li>
  </ul>
</template>
<style lang='scss'>
.demo2 {
  .row {
    overflow: hidden;
  }

  ul {
    list-style: none;
    margin: 0;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 5px;
    margin-bottom: 20px;
  }

  .list-group.list-group-child {
    padding: 5px 0 5px 5px;
    margin-left: 15px;
    margin-bottom: 5px;
  }

  .list-group > li {
    padding: 0;
    margin-bottom: 5px;
  }

  .list-group-child > li > .name {
    background-color: var(--devui-base-bg);
  }

  .list-group > li > .name {
    width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    padding: 0 10px;
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
    border-radius: 4px;
  }

  .list-group > li.drag-over-item > .name {
    background-color: var(--devui-list-item-active-bg);
    color: var(--devui-list-item-active-text);
  }
}

</style>

:::

드래그 시 실제 요소 따라다니기

:::demo 드래그할 때 반투명하게 처리되지 않은 실제 요소가 따라오도록 할 수 있어요. appendToBody도 사용할 수 있어요: 드래그한 요소가 떠난 뒤 원래 위치의 부모 객체가 소멸될 수 있다면, 클론이 소멸되지 않도록 body에 붙여야 해요. 기본적으로 스타일을 복사해서 body로 클론된 실체의 스타일이 올바르게 유지되지만, DOM 노드 위치에 깊이 의존하는 일부 스타일과 속성은 제대로 적용되지 않을 수 있으니 일부 스타일은 직접 조정해야 해요.

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const data= reactive({
      list1: [
        { name: 'Visual Studio Code' },
        { name: 'WebStorm' },
        { name: 'Sublime' },
        { name: 'Atom' }
      ],
      list2: [],
    });

    const appendToBody = ref(false);

    function onDrop(e) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      if (-1 !== index) {
        /* 修正同一个container排序,往下拖动index多了1个位置*/
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        data.list2.splice(index, 0, fromIndex === -1 ? e.dragData : data.list2.splice(fromIndex, 1)[0]);
      } else {
        data.list2.push(e.dragData);
      }
      if (fromIndex === -1) {
        removeItem(e.dragData, data.list1);
      }
    }

    function removeItem(item, list) {
      const index = list.map(function (e) {
        return e.name;
      }).indexOf(item.name);
      list.splice(index, 1);
    }
    return { onDrop, data, appendToBody };
  },
});
</script>
<template>
<div class="demo3">
  <div class="row">
    <div class="col-sm-3">
      <div class="card">
        <div class="card-header">Draggable Items</div>
        <div class="card-block">
          <ul class="list-group">
            <li
              v-dDraggable="{
                enableDragFollow: true,
                dragFollowOptions: {appendToBody: appendToBody},
                dragOverClass: 'box-shadow',
                dragScope: 'default-css',
                dragData: item,
              }"
              v-for="item in data.list1"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div class="card card-outline-primary mb-3"
        v-dDroppable="{
          dropScope: 'default-css',
          '@dropEvent': ($event) => onDrop($event),
        }">
        <div class="card-header">Drop Area with Sortable</div>
        <div class="card-block">
          <ul class="list-group d-drop-el" v-dSortable>
            <li
              v-dDraggable="{
                enableDragFollow: true,
                dragFollowOptions: {appendToBody: appendToBody},
                dragOverClass: 'box-shadow',
                dragScope: 'default-css',
                dragHandleClass: 'drag-handle',
                dragData: item,
              }"
              v-for="item in data.list2"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
  <div class="option">
    <div class="option-toggle">
      <d-switch v-model="appendToBody"></d-switch>
      <span style="margin-left: 8px">appendToBody</span>
    </div>
  </div>

</div>

</template>
<style lang='scss'>
.demo3 {
  
  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .box-shadow {
    box-shadow: var(--devui-shadow-length-hover) var(--devui-hover-shadow);
    border-right: 10px red solid;
    border-bottom: 12px red solid;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }

  .option {
    line-height: 1.5;
    vertical-align: middle;
    margin-top: 20px;

    .option-toggle {
      margin-bottom: 8px;
    }
  }

  .option > d-toggle {
    display: inline-block;
    transform: translateY(3px);
  }

}

</style>

:::

경계를 넘을 때 위치 교환

:::demo switchWhileCrossEdge를 설정하면 경계를 넘어가는 순간 위치를 맞바꿀 수 있어요. 주의: dropOnItem과 함께 사용할 수 없고, dropOnItem이 true일 때는 효과가 없어요.

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const data= reactive({
      list: [
        { name: 'Visual Studio Code' },
        { name: 'WebStorm' },
        { name: 'Sublime' },
        { name: 'Atom' }
      ],
    });

    function onDrop(e) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      if (-1 !== index) {
        /* 修正同一个container排序,往下拖动index多了1个位置*/
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        data.list.splice(index, 0, fromIndex === -1 ? e.dragData : data.list.splice(fromIndex, 1)[0]);
      } else {
        data.list.push(e.dragData);
      }
    }
    return { onDrop, data };
  },
});
</script>
<template>
<div class="demo4">
  <div class="row">
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          switchWhileCrossEdge: true,
          dropScope: 'default-switch',
          '@dropEvent': onDrop,
        }"
      >
        <div class="card-header">Drag Drop Sort Area</div>
        <div class="card-block">
          <ul class="list-group drop-el" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-switch',
                dragHandleClass: 'drag-handle',
                dragData: item,
              }"
              v-for="item in data.list"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>

</div>

</template>
<style lang='scss'>
.demo4 {
  
  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }
}

</style>

:::

외부 놓기 위치: 가까운 곳, 앞, 뒤

:::demo defaultDropPostion을 사용하면 정렬기 바깥 영역에 요소를 놓을 때 목록의 앞에 추가할지 뒤에 추가할지 기본 동작을 정할 수 있어요. 기본값은 가까운 위치('closest')예요.

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const data= reactive({
      list1: [{ name: 'Visual Studio Code' } ],
      list2: [{ name: 'WebStorm' }],
      list3: [{ name: 'Sublime' }],
      list4: [{ name: 'Atom' }],
    });

    function onDrop(e, targetArray) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const parentArray = e.dragData.parent;
      const item = e.dragData.item;
      if (-1 !== index) {
        if (-1 !== fromIndex && index > fromIndex) { index--; }
        targetArray.splice(index, 0, fromIndex === -1 ? item : targetArray.splice(fromIndex, 1)[0]);
      } else {
        targetArray.push(item);
      }
      if (fromIndex === -1) {
        removeItem(item, parentArray);
      }
    }

    function removeItem(item, list) {
      const index = list.map(function (e) {
        return e.name;
      }).indexOf(item.name);
      list.splice(index, 1);
    }
    return { onDrop, data };
  },
});
</script>
<template>
<div class="demo5">
  <div class="row">
    <div class="col-sm-3">
      <div class="card">
        <div class="card-header">Items</div>
        <div class="card-block">
          <ul class="list-group">
            <li
              v-dDraggable="{
                dragScope: 'default-position',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parent: data.list1 },
              }"
              v-for="item in data.list1"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          switchWhileCrossEdge: true,
          dropScope: 'default-position',
          defaultDropPosition: 'before',
          '@dropEvent': (e) => onDrop(e, data.list2),
        }"
      >
        <div class="card-header">Drop here to add to the top</div>
        <div class="card-block">
          <ul class="list-group drop-el" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-position',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parent: data.list2 },
              }"
              v-for="item in data.list2"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          switchWhileCrossEdge: true,
          dropScope: 'default-position',
          defaultDropPosition: 'after',
          '@dropEvent': (e) => onDrop(e, data.list3),
        }"
      >
        <div class="card-header">Drop here to add to the bottom</div>
        <div class="card-block">
          <ul class="list-group drop-el" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-position',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parent: data.list3 },
              }"
              v-for="item in data.list3"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          switchWhileCrossEdge: true,
          dropScope: 'default-position',
          defaultDropPosition: 'closest',
          '@dropEvent': (e) => onDrop(e, data.list4),
        }"  
      >
        <div class="card-header">Drop here to add to the nearest place (top)</div>
        <div class="card-block">
          <ul class="list-group drop-el" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-position',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parent: data.list4 },
              }"
              v-for="item in data.list4"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
        <div class="card-footer">Drop here to add to the nearest place (bottom)</div>
      </div>
    </div>
  </div>
</div>

</template>
<style lang='scss'>
.demo5 {
  
  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }
}

</style>

:::

드래그 스크롤 컨테이너 강화

:::demo dDropScrollEnhanced 디렉티브와 함께 사용하면, 드래그 중 가장자리에 닿았을 때 스크롤바가 양쪽 방향으로 가속되어 굴러가요.

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const lists = reactive([
      {
        name: 'IDE',
        list:   [
          { name: 'Visual Studio Code' },
          { name: 'WebStorm' },
          { name: 'Sublime Text' },
          { name: 'Atom' },
          { name: 'Notepad++' },
        ]
      },
      {
        name: 'Browser',
        list:   [
          { name: 'Chrome' },
          { name: 'Firefox' },
          { name: 'Opera' },
          { name: 'Edge' },
          { name: 'Internet Explorer' },
          { name: 'Safari' },
        ]
      },
      {
        name: 'OS',
        list: [
          {name: 'Linux'},
          {name: 'Windows'},
          {name: 'Mac OS'},
          {name: 'DOS'},
          {name: 'Chrome OS'},
        ]
      },
      {
        name: 'Mobile OS',
        list:  [
          {name: 'Android'},
          {name: 'IOS'},
          {name: 'BlackBerry'},
          {name: 'Symbian'},
        ]
      },
      {
        name: 'Website',
        list: []
      },
      {
        name: 'Search Engine',
        list: []
      },
      {
        name: 'Technology Stack',
        list: []
      },
      {
        name: 'Language',
        list: []
      },
      {
        name: 'Whatever',
        list: []
      },
    ]);

    const showDropScrollArea = ref(false);

    function onDrop(e, targetArray) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const parentArray = e.dragData.parent;
      const item = e.dragData.item;
      if (-1 !== index) {
        if (-1 !== fromIndex && index > fromIndex) { index--; }
        targetArray.splice(index, 0, fromIndex === -1 ? item : targetArray.splice(fromIndex, 1)[0]);
      } else {
        targetArray.push(item);
      }
      if (fromIndex === -1) {
        removeItem(item, parentArray);
      }
    }

    function removeItem(item, list) {
      const index = list.map(function (e) {
        return e.name;
      }).indexOf(item.name);
      list.splice(index, 1);
    }
    return { onDrop, lists, showDropScrollArea };
  },
});
</script>
<template>
<div class="demo6">
  <div :class="{ 'show-drop-area': showDropScrollArea }">
    <div class="drop-row devui-scrollbar" 
      v-dDropScrollEnhanced="{
        direction: 'h',
        dropScrollScope: ['multiple-group', 'drag-follow'],
      }">
      <div class="drop-col" v-for="group in lists">
        <div class="card card-outline-primary mb-3"
          v-dDroppable="{
            dropScope: 'multiple-group',
            '@dropEvent': (e) => onDrop(e, group.list),
          }">
          <div class="card-header">{{ group.name }}</div>
          <div class="card-block">
            <ul
              class="list-group drop-el devui-scrollbar"
              v-dSortable
              v-dDropScrollEnhanced="{
                direction: 'v',
                responseEdgeWidth: '20px',
                dropScrollScope: 'multiple-group',
              }"
            >
              <li
                v-dDraggable="{
                  dragScope: 'multiple-group',
                  dragHandleClass: 'drag-handle',
                  dragData: { item: item, parent: group.list },
                }"
                v-for="item in group.list"
                class="list-group-item over-flow-ellipsis"
              >
                {{ item.name }}
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>
    <div class="option-toggle">
      <d-switch v-model="showDropScrollArea"></d-switch>
      <span>Show drop scroll response area</span>
    </div>
    <template v-if="showDropScrollArea">
      <div>
        <span class="color-example active" style="display: inline-block; width: 25px; height: 14px; margin-right: 4px"></span>
        Active area, dragging over it to trigger scroll and show more.
      </div>
      <div>
        <span class="color-example inactive" style="display: inline-block; width: 25px; height: 14px; margin-right: 4px"></span>
        Inactive area, dragging over it will be able to drop to element under the cover.
      </div>
    </template>
  </div>

</div>

</template>
<style lang='scss'>
.demo6 {

  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drop-row {
    white-space: nowrap;
    overflow-x: auto;
    margin-bottom: 10px;
  }

  .drop-col {
    margin-left: 10px;
    display: inline-block;
    vertical-align: top;
    width: 180px;
    padding-bottom: 10px;

    &:last-of-type {
      margin-right: 10px;
    }
  }

  .show-drop-area .dropover-scroll-area,
  .color-example {
    opacity: 0.1;
  }

  .show-drop-area .dropover-scroll-area.active,
  .color-example.active {
    background: red;
  }

  .show-drop-area .dropover-scroll-area.inactive,
  .color-example.inactive {
    background: blue;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
    max-height: 100px;
    overflow-y: auto;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }

  .option-toggle {
    display: flex;
    align-items: center;
    margin-bottom: 8px;

    span {
      margin-left: 4px;
    }
  }
}

</style>

:::

원본 플레이스홀더

:::demo originPlaceholder로 원래 위치에 플레이스홀더를 표시할 수 있어요. 예시는 사라지는 애니메이션이에요.

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const data = reactive({
      list1: [
        { name: 'Visual Studio Code' },
        { name: 'WebStorm' },
        { name: 'Sublime Text' },
        { name: 'Atom' },
        { name: 'Notepad++' },
      ],
      list2: [
        { name: 'Chrome' },
        { name: 'Firefox' },
        { name: 'Opera' },
        { name: 'Edge' },
        { name: 'Internet Explorer' },
        { name: 'Safari' },
      ],
      list3: [
        {name: 'Linux'},
        {name: 'Windows'},
        {name: 'Mac OS'},
        {name: 'DOS'},
        {name: 'Chrome OS'},
      ],
    });

    const showDropScrollArea = ref(false);

    function onDrop(e, targetArray) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const parentArray = e.dragData.parent;
      const item = e.dragData.item;
      if (-1 !== index) {
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        targetArray.splice(index, 0, fromIndex === -1 ? item : targetArray.splice(fromIndex, 1)[0]);
      } else {
        targetArray.push(item);
      }
      if (fromIndex === -1) {
        removeItem(item, parentArray);
      }
    }

    function removeItem(item, list) {
      const index = list.indexOf(item);
      list.splice(index, 1);
    }
    return { onDrop, data, showDropScrollArea };
  },
});
</script>
<template>
<div class="demo7">
  <div class="row">
    <div class="col-sm-3">
      <div class="card"
        v-dDroppable="{
          dropScope: 'default-css',
          '@dropEvent': (e) => { onDrop(e, data.list1) },
        }">
        <div class="card-header">Drag from here,origin placeholder disappear <strong>directly</strong> after dropped</div>
        <div class="card-block">
          <ul class="list-group drop-el" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-css',
                dragData: { item: item, parent: data.list1 },
                originPlaceholder: {
                  text: 'custom text and style',
                  style: { backgroundColor: '#ffe6e6', textAlign: 'center' }
                },
              }"
              v-for="item in data.list1"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          dropScope: 'default-css',
          placeholderStyle: {},
          '@dropEvent': (e) => { onDrop(e, data.list2) },
        }"
      >
        <div class="card-header">Drag from here,origin placeholder <strong>delay to</strong> disappear after dropped</div>
        <div class="card-block">
          <ul class="list-group drop-el" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-css',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parent: data.list2 },
                originPlaceholder: {
                  removeDelay: 100,
                },
              }"
              v-for="item in data.list2"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          dropScope: 'default-css',
          placeholderStyle: {},
          '@dropEvent': (e) => { onDrop(e, data.list3) },
        }"
      >
        <div class="card-header">
          Drag from here,origin placeholder <strong>delay to</strong> disappear after dropped <strong>with animation</strong>
        </div>
        <div class="card-block">
          <ul class="list-group drop-el list-3" v-dSortable>
            <li
              v-dDraggable="{
                dragScope: 'default-css',
                dragHandleClass: 'drag-handle',
                dragData: { item: item, parent: data.list3 },
                originPlaceholder: {
                  removeDelay: 300,
                },
              }"
              v-for="item in data.list3"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>

</div>

</template>
<style lang='scss'>
.demo7 {

  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }

  .drag-placeholder {
    border: 1px dashed var(--devui-brand);
    margin: 10px 0;
  }

  .drag-origin-placeholder {
    border: 1px dashed var(--devui-line);
    margin: 10px 0;
  }

  .drop-el li:first-child + .drag-origin-placeholder,
  .drag-origin-placeholder:first-child {
    margin-top: 0;
  }

  .drag-origin-placeholder.hit-origin-placeholder {
    border-color: var(--devui-brand);
  }

  .list-3 .drag-origin-placeholder.delay-deletion {
    animation: origin-placeholder-shrink-to-none 0.2s cubic-bezier(0.86, 0, 0.07, 1) 1 0.05s both;
  }

  @keyframes origin-placeholder-shrink-to-none {
    to {
      height: 0;
    }
  }
}

</style>

:::

드래그 미리보기

:::demo 드래그할 때 원하는 스타일의 드래그 요소를 보여줄 수 있어요.

<script>
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  setup() {
    const data= reactive({
      list1: [
        { name: 'Visual Studio Code' },
        { name: 'WebStorm' },
        { name: 'Sublime' },
      ],
      list2: [
        { name: 'Atom' },
      ],
    });

    function onDrop(e) {
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      if (-1 !== index) {
        /* 修正同一个container排序,往下拖动index多了1个位置*/
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        data.list2.splice(index, 0, fromIndex === -1 ? e.dragData : data.list2.splice(fromIndex, 1)[0]);
      } else {
        data.list2.push(e.dragData);
      }
      if (fromIndex === -1) {
        removeItem(e.dragData, data.list1);
      }
    }

    function removeItem(item, list) {
      const index = list.map(function (e) {
        return e.name;
      }).indexOf(item.name);
      list.splice(index, 1);
    }

    const previewRef = ref();

    return { onDrop, data, previewRef };
  },
});
</script>
<template>
<div class="demo9">
  <div class="row">
    <div class="col-sm-3">
      <div class="card">
        <div class="card-header">Draggable Items</div>
        <div class="card-block">
          <ul class="list-group">
          <d-drag-preview-template ref="previewRef" v-slot="previewContext">
            <div class='drag-preview-item'>
              <span class='index'>{{previewContext.data.index + 1}}</span>
              <span class='name'>{{previewContext.dragData.name}}</span>
            </div>
          </d-drag-preview-template>
            <li
              v-dDragPreview="{
                dragPreview: previewRef,
                dragPreviewData: { index: i },
              }"
              v-dDraggable="{
                dragScope: 'drag-preview',
                dragData: item,
              }"
              v-for="(item, i) in data.list1"
              class="list-group-item over-flow-ellipsis"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-sm-3">
      <div class="card card-outline-primary mb-3"
        v-dDroppable="{
          dropScope: 'drag-preview',
          '@dropEvent': ($event) => onDrop($event),
        }">
        <div class="card-header">Drop Area with Sortable</div>
        <div class="card-block">
          <ul class="list-group d-drop-el" v-dSortable>
            <li
              v-dDraggable="{
                enableDragFollow: true,
                dragFollowOptions: {appendToBody: true},
                dragOverClass: 'box-shadow',
                dragScope: 'drag-preview',
                dragHandleClass: 'drag-handle',
                dragData: item,
              }"
              v-for="(item, i) in data.list2"
              class="list-group-item-new over-flow-ellipsis"
            >
              <span class='index'>{{ i + 1}}</span>
              <span class='name'>{{ item.name }}</span>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</div>

</template>
<style lang='scss'>
.demo9 {
  
  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .box-shadow {
    box-shadow: var(--devui-shadow-length-hover) var(--devui-hover-shadow);
    border-right: 10px red solid;
    border-bottom: 12px red solid;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
  }

  .list-group > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }
}
// 使得拖拽起来的元素和目标元素的样式一样
.demo9 .list-group > li.list-group-item-new,
.drag-preview-item {
  position: relative;
  width: 160px;
  height: 50px;
  border: 1px solid var(--devui-brand);
  border-radius: 4px;
  background-color: var(--devui-base-bg);
  color: var(--devui-text);
  margin-bottom: 5px;
  line-height: 1.5;
  display: flex;
  align-items: center;
  .index {
    position: absolute;
    left: 0;
    top: 0;
    width: 20px;
    height: 20px;
    border-radius: 4px;
    color: var(--devui-light-text);
    background-color: var(--devui-brand);
    text-align: center;
  }
  .name {
    padding-left: 28px;
  }
}

</style>

:::

일괄 드래그

:::demo batchDrag 디렉티브로 표시하면 여러 요소를 한꺼번에 끌 수 있어요. ctrl 키를 누른 채 마우스로 여러 개를 선택한 다음 드래그해 보세요.

<script>
import { defineComponent, reactive, ref, inject, nextTick } from 'vue';

export default defineComponent({
  setup() {
    const data = reactive([
      {
        name: 'IDE',
        list: [
          { name: 'Visual Studio Code', isSelected: false },
          { name: 'WebStorm', isSelected: false },
          { name: 'Sublime Text', isSelected: false },
          { name: 'Atom', isSelected: false },
          { name: 'Notepad++', isSelected: false }
        ],
      },
      {
        name: 'Browser',
        list: [
          { name: 'Chrome', isSelected: false },
          { name: 'Firefox', isSelected: false },
          { name: 'Opera', isSelected: false },
          { name: 'Edge', isSelected: false },
          { name: 'Internet Explorer', isSelected: false },
          { name: 'Safari', isSelected: false },
        ],
      },
      {
        name: 'OS',
        list: [
          { name: 'Linux', isSelected: false },
          { name: 'Windows', isSelected: false },
          { name: 'Mac OS', isSelected: false },
          { name: 'DOS', isSelected: false },
          { name: 'Chrome OS', isSelected: false }
        ],
      },
      {
        name: 'Mobile OS',
        list: [
          { name: 'Android', isSelected: false },
          { name: 'IOS', isSelected: false },
          { name: 'BlackBerry', isSelected: false },
          { name: 'Symbian', isSelected: false }
        ],
      },
      {
        name: 'Whatever',
        list: [],
      },
    ]);

    const showOriginPlaceholder = ref(false);
    const switchWhileCrossEdge = ref(false);

    function onDrop(e, targetArray) {
      if (e.dropOnOrigin) {
        return;
      }
      if (e.batchDragData) {
        batchDrop(e, targetArray);
        return;
      }
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const parentArray = e.dragData.parent;
      const item = e.dragData.item;
      if (-1 !== index) {
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        targetArray.splice(index, 0, fromIndex === -1 ? item : targetArray.splice(fromIndex, 1)[0]);
      } else {
        targetArray.push(item);
      }
      if (fromIndex === -1) {
        removeItem(item, parentArray);
      }
    }

    function batchDrop(e, targetArray) {
      let fromIndexLessThanDropIndexCount = 0;
      e.batchDragData
        .map((dragData) => {
          const index = targetArray.indexOf(dragData.item);
          if (index > -1 && index < e.dropIndex) {
            fromIndexLessThanDropIndexCount++;
          }
          return dragData;
        })
        .forEach((dragData) => {
          removeItem(dragData.item, dragData.parent);
        });
      targetArray.splice(e.dropIndex - fromIndexLessThanDropIndexCount, 0, ...e.batchDragData.map((batchItem) => batchItem.item));
      return;
    }

    function removeItem(item, list) {
      const index = list.indexOf(item);
      list.splice(index, 1);
    }

    function batchSelect(item) {
      item.isSelected = !(item.isSelected || false);
    }

    function batchSelectCheck(event, item) {
      if (event.ctrlKey) {
        batchSelect(item);
      }
    }

    function cleanBatch() {
      data.forEach((list) => list.list.forEach((item) => {item['isSelected'] = false;}));
    }

    return { onDrop, data, showOriginPlaceholder, switchWhileCrossEdge, batchSelect, batchSelectCheck, cleanBatch };
  },
});
</script>
<template>
<div class="demo8">
  <div class="drop-row devui-scrollbar"
    v-dDropScrollEnhanced="{
      direction: 'h',
      dropScrollScope: ['multiple-group', 'drag-follow'],
    }">
    <div class="drop-col" v-for="list in data">
      <div
        class="card card-outline-primary mb-3"
        v-dDroppable="{
          dropScope: 'multiple-group',
          switchWhileCrossEdge: switchWhileCrossEdge,
          '@dropEvent': (e) => onDrop(e, list.list),
        }"
      >
        <div class="card-header">{{ list.name }}</div>
        <div class="card-block">
          <ul
            class="list-group drop-el devui-scrollbar"
            v-dSortable
            v-dDropScrollEnhanced="{
              direction: 'v',
              responseEdgeWidth: '20px',
              dropScrollScope: 'multiple-group',
            }"
          >
            <li
              v-for="item in list.list" :key="item"
              class="list-group-item over-flow-ellipsis"
              v-dDraggable="{
                dragIdentity: item, // 注意:由于使用了动态组合的dragData,批量拖拽需要使用dragIdentity标记唯一标识符, 否则每次刷新的dragData后会不一致,影响排序
                dragScope: 'multiple-group',
                dragHandleClass: 'drag-handle',
                originPlaceholder: {
                  show: showOriginPlaceholder,
                  removeDelay: 300
                },
                dragData: { item: item, parent: list.list },
                enableDragFollow: true,
              }"
              @click="(e) => batchSelectCheck(e, item)"
              v-dDraggableBatchDrag="{
                batchDragActive: item.isSelected,
                '@batchDragActiveEvent': () => {batchSelect(item)},
              }"
              :class="{ 'selected': item.isSelected }"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
  <d-switch v-model="showOriginPlaceholder"></d-switch> show origin placeholder
  <d-switch v-model="switchWhileCrossEdge"></d-switch> cross edge switch
  <d-button variant="solid" @click="cleanBatch"> clean all selected </d-button>
</div>

</template>
<style lang='scss'>
.demo8 {
  .row {
    overflow: hidden;
  }

  .col-sm-3 {
    float: left;
    position: relative;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
    min-width: 220px;
    width: 220px;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drop-row {
    white-space: nowrap;
    overflow-x: auto;
    margin-bottom: 10px;
  }

  .drop-col {
    margin-left: 10px;
    display: inline-block;
    vertical-align: top;
    width: 180px;
    padding-bottom: 10px;
  }

  .drag-border {
    border: var(--devui-brand) dashed 1px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .drag-hint-border {
    border: var(--devui-success) dashed 2px;
  }

  .drag-over-border {
    border: var(--devui-warning) dashed 2px;
  }

  .card {
    border: 1px solid var(--devui-dividing-line);
  }

  .card .card-header {
    font-size: var(--devui-font-size-card-title);
    padding: 10px;
  }

  .list-group {
    padding: 10px;
    max-height: 300px;
    overflow-y: auto;
  }

  .list-group > li,
  .list-group > div > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }

  .list-group > li.selected,
  .list-group > div > li.selected {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-brand);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-list-item-selected-bg);
    display: flex;
    align-items: center;
  }

  .drag-placeholder {
    border: 1px dashed var(--devui-brand);
    margin: 10px 0;
  }

  .show-origin .drag-origin-placeholder:not(.side-drag-origin-placeholder) {
    border: 1px dashed var(--devui-line);
  }

  .show-origin .drag-origin-placeholder {
    margin: 10px 0;
  }

  .show-origin .drag-origin-placeholder:first-child {
    margin-top: 0;
  }

  .show-origin .drag-origin-placeholder.hit-origin-placeholder {
    border-color: var(--devui-brand--);
  }

  .show-origin .drag-origin-placeholder.delay-deletion {
    animation: origin-placeholder-shrink-to-none 0.2s cubic-bezier(0.86, 0, 0.07, 1) 1 0.05s both;
  }

  @keyframes origin-placeholder-shrink-to-none {
    to {
      height: 0;
    }
  }
}
.show-origin .drop-el li:first-child + .drag-origin-placeholder {
  margin-top: 0;
}

</style>

:::

2차원 드래그와 조합 드래그 미리보기

:::demo dDragDropSyncBox 디렉티브, dDragSync 디렉티브, dDropSync 디렉티브로 협업 드래그를 구성해 2차원 드래그를 구현해요. dDragPreview로 드래그 미리보기를 구성하고, d-drag-preview-clone-dom-ref로 드래그 노드 미리보기 클론을 완성해요.

<script>
import { defineComponent, reactive, ref, inject, nextTick } from 'vue';

export default defineComponent({
  setup() {
    const data = reactive({
      lists: {
        listA1: [
          { name: 'Visual Studio Code' },
          { name: 'WebStorm' },
          { name: 'Sublime Text' },
          { name: 'Atom' },
          { name: 'Notepad++' },
        ],
        listA2:   [
          { name: 'Chrome' },
          { name: 'Firefox' },
          { name: 'Opera' },
          { name: 'Edge' },
          { name: 'Internet Explorer' },
          { name: 'Safari' },
        ],
        listA3: [],
        listB1: [
          {name: 'Linux'},
          {name: 'Windows'},
          {name: 'Mac OS'},
          {name: 'DOS'},
          {name: 'Chrome OS'},
        ],
        listB2:  [
          {name: 'Android'},
          {name: 'IOS'},
          {name: 'BlackBerry'},
          {name: 'Symbian'},
        ],
        listB3: [],
        listC1: [],
        listC2: [],
        listC3: []
      },
    });
    const meta = reactive({
      owners:[ {
        id: 'not-assign',
        name: 'Not Assign',
        collapse: false
      }, {
        id: 'available',
        name: 'Available',
        collapse: false
      }, {
        id: 'not-available',
        name: 'Not Available',
        collapse: false
      }],
      listCol:[
        {
          id: 'group1',
          name: 'Group 1'
        }, {
          id: 'group2',
          name: 'Group 2'
        }, {
          id: 'group3',
          name: 'Group 3'
        }
      ],
      ownerListMap: {
        'not-assign': {
          group1: data.lists.listA1,
          group2: data.lists.listA2,
          group3: data.lists.listA3,
        },
        'available': {
          group1: data.lists.listB1,
          group2: data.lists.listB2,
          group3: data.lists.listB3,
        },
        'not-available': {
          group1: data.lists.listC1,
          group2: data.lists.listC2,
          group3: data.lists.listC3,
        },
      }
    });

    function onDrop(e, targetArray) {
      if (e.dropOnOrigin) {
        return;
      }
      if (e.batchDragData) {
        batchDrop(e, targetArray);
        return;
      }
      let index = e.dropIndex;
      const fromIndex = e.dragFromIndex;
      const parentArray = e.dragData.parent;
      const item = e.dragData.item;
      if (-1 !== index) {
        if (-1 !== fromIndex && index > fromIndex) {
          index--;
        }
        targetArray.splice(index, 0, fromIndex === -1 ? item : targetArray.splice(fromIndex, 1)[0]);
      } else {
        targetArray.push(item);
      }
      if (fromIndex === -1) {
        removeItem(item, parentArray);
      }
    }

    function batchDrop(e, targetArray) {
      let fromIndexLessThanDropIndexCount = 0;
      e.batchDragData
        .map((dragData) => {
          const index = targetArray.indexOf(dragData.item);
          if (index > -1 && index < e.dropIndex) {
            fromIndexLessThanDropIndexCount++;
          }
          return dragData;
        })
        .forEach((dragData) => {
          removeItem(dragData.item, dragData.parent);
        });
      targetArray.splice(e.dropIndex - fromIndexLessThanDropIndexCount, 0, ...e.batchDragData.map((batchItem) => batchItem.item));
      return;
    }

    function removeItem(item, list) {
      const index = list.indexOf(item);
      list.splice(index, 1);
    }

    function batchSelect(item) {
      item.isSelected = !(item.isSelected || false);
    }

    function batchSelectCheck(event, item) {
      if (event.ctrlKey) {
        batchSelect(item);
      }
    }

    function cleanBatch() {
      data.forEach((list) => list.list.forEach((item) => {item['isSelected'] = false;}));
    }

    const colTemplate = ref();

    return { onDrop, data, meta, batchSelect, batchSelectCheck, cleanBatch, colTemplate };
  },
});
</script>
<template>
<div class="demo10">
  <div class="board" 
    v-dDragDropSyncBox
    v-dDropScrollEnhanced="{
      direction: 'v',
    }" 
    v-dDropScrollEnhancedSide="{
      direction: 'v',
    }">
    <div class="row row-group">
      <div class="row title-row occupied-width">
        <div class="col-group">
          <div class="col title-col">Status</div>
        </div>
        <div
          class="col-group"
          v-dDroppable="{
            dropScope: 'col',
            switchWhileCrossEdge: true,
            '@dropEvent': (e) => {onDrop(e, meta.listCol)},
          }"
          v-dSortable="{
            dSortable: 'h'
          }"
          v-dDropSortSync="{
            dropSortSync: 'col',
          }"
        >
          <div
            class="col"
            v-dDragPreview="{
              dragPreview: colTemplate,
            }"
            v-dDraggable="{
              dragScope: 'col',
              dragData:{ item: col, parent: meta.listCol },
            }"
            v-dDragSync="{
              dragSync: 'col-' + col.id,
            }"
            v-for="col in meta.listCol"
          >
            {{ col.name }}
          </div>
        </div>
      </div>
    </div>
    <div class="row row-group"
      v-dSortable
      v-dDroppable="{
        dropScope: 'row',
        switchWhileCrossEdge: true,
        '@dropEvent': (e) => onDrop(e, meta.owners),
      }">
      <div
        class="row"
        v-dDraggable="{
          dragScope: 'row',
          dragHandle: '.title-col',
          enableDragFollow: true,
          dragData: { item: owner, parent: meta.owners },
        }"
        v-for="owner in meta.owners"
      >
        <div class="col-group">
          <div class="col title-col">{{ owner.name }}</div>
        </div>
        <div class="col-group"
          v-dDropSortSync="{
            dropSortSync: 'col',
          }">
          <div class="col"
            v-dDragSync="{
              dragSync: 'col-' + col.id,
            }" 
            v-for="col in meta.listCol"
          >
            <div class="card-container"
              v-dDroppable="{
                dropScope: 'card-container',
                switchWhileCrossEdge: true,
                '@dropEvent': (e) => onDrop(e, meta.ownerListMap[owner.id][col.id]),
              }">
              <ul class="list-group drop-el" v-dSortable>
                <li
                  v-for="item of meta.ownerListMap[owner.id][col.id]"
                  class="list-group-item over-flow-ellipsis"
                  v-dDraggable="{
                    dragIdentity: item, // 批量脱欧拽必要
                    dragScope: 'card-container',
                    dragData: { item: item, parent: meta.ownerListMap[owner.id][col.id] },
                    enableDragFollow: true,
                  }"
                  @click="(e) => batchSelectCheck(e, item)"
                  v-dDraggableBatchDrag="{
                    batchDragActive: item.isSelected,
                    '@batchDragActiveEvent': () => { batchSelect(item) },
                  }"
                  :class="{ selected: item.isSelected }"
                >
                  {{ item.name }}
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <d-drag-preview-template ref="colTemplate" v-slot="{draggedEl, dragSyncDOMElements}">
    <div class="board board-preview">
      <div class="row title-row">
        <d-drag-preview-clone-dom-ref :domRef="draggedEl"></d-drag-preview-clone-dom-ref>
      </div>
      <div class="row" v-for="dom in dragSyncDOMElements">
        <d-drag-preview-clone-dom-ref :domRef="dom"></d-drag-preview-clone-dom-ref>
      </div>
    </div>
  </d-drag-preview-template>
</div>

</template>
<style lang='scss'>
.board {
  width: 100%;
  max-height: 500px;
  overflow: auto;
  background-color: var(--devui-area);
  padding: 5px;

  .row {
    display: flex;
    margin: 10px 0;
    width: 100%;
    white-space: nowrap;

    .col {
      display: inline-flex;
      flex-shrink: 0; /* fix ie 11 auto shrink to default value 1 */
      vertical-align: top;
      align-items: stretch;
      min-height: 75px;
      width: 285px;
      margin: 0 5px;
      background-color: var(--devui-base-bg);
      box-shadow: var(--devui-shadow-length-base) var(--devui-shadow);
    }
  }
  &.board-preview {
    background-color: transparent;
    opacity: 0.95;
    overflow: visible;
    max-height: initial;
  }
}

.demo10 {
  .board {
    .row-group {
      display: block;
      width: 100%;
    }

    .col-group {
      white-space: nowrap;
      display: inline-flex;
      flex-shrink: 0; /* fix ie 11 auto shrink to default value 1 */
      vertical-align: top;
      align-items: stretch;

      .col:last-of-type {
        margin-right: 10px;
      }
    }
  }

  /* 配色 */
  .board {
    .row.title-row .col {
      font-weight: bold;
    }

    .col.title-col {
      font-weight: bold;
    }
  }

  /* 行折叠 */
  .board .row.row-collapse {
    max-height: 100px;

    .col {
      max-height: 100%;
      overflow: hidden;
    }
  }

  /* 列折叠 */
  .board .col.col-collapse {
    max-width: 100px;
    overflow: hidden;
  }

  /* 样式修正 */
  .board .col-group > .drag-placeholder {
    display: inline-block;
    vertical-align: top;
    align-items: stretch;
    margin: 0 5px;
  }

  .row.occupied-width {
    min-width: 100%;
  }

  /* 卡片容器 */
  .card-container {
    width: 100%;
    height: 100%;
  }

  .list-group .list-group-item {
    padding: 0 16px;
    border-radius: 4px;
  }

  .drag-handle {
    cursor: move;

    /* fallback if grab cursor is unsupported */
    cursor: grab;
    cursor: -moz-grab;
    cursor: -webkit-grab;
  }

  .drag-handle:active {
    cursor: grabbing;
    cursor: -moz-grabbing;
    cursor: -webkit-grabbing;
  }

  .list-group {
    padding: 10px;
  }

  .list-group > li,
  .list-group > div > li {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-text);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-base-bg);
    display: flex;
    align-items: center;
  }

  .list-group > li.selected,
  .list-group > div > li.selected {
    height: 30px;
    border: 1px solid var(--devui-brand);
    color: var(--devui-brand);
    margin-bottom: 5px;
    line-height: 1.5;
    background-color: var(--devui-list-item-selected-bg);
    display: flex;
    align-items: center;
  }

  .drag-placeholder {
    border: 1px dashed var(--devui-brand);
    margin: 10px 0;
  }

  .show-origin .drag-origin-placeholder:not(.side-drag-origin-placeholder) {
    border: 1px dashed var(--devui-line);
  }

  .show-origin .drag-origin-placeholder {
    margin: 10px 0;
  }

  .show-origin .drop-el li:first-child + .drag-origin-placeholder,
  .show-origin .drag-origin-placeholder:first-child {
    margin-top: 0;
  }

  .show-origin .drag-origin-placeholder.hit-origin-placeholder {
    border-color: var(--devui-brand);
  }

  .show-origin .drag-origin-placeholder.delay-deletion {
    animation: origin-placeholder-shrink-to-none 0.2s cubic-bezier(0.86, 0, 0.07, 1) 1 0.05s both;
  }

  @keyframes origin-placeholder-shrink-to-none {
    to {
      height: 0;
    }
  }
}


</style>

:::

dDraggable 디렉티브

dDraggable 파라미터
파라미터 타입 기본값 설명 Demo 링크
dragData any -- 선택, DropEvent 이벤트에 전달할 데이터 기본 사용법
dragScope string | Array<string> 'default' 선택, drop 위치 제한. 대응하는 dropScope와 일치해야 함 기본 사용법
dragOverClass string -- 선택, 드래그 중 피드래그 요소의 css 실체 요소 드래그 따라가기
dragHandle string -- 선택, 드래그 가능 콘텐츠의 css 선택자. css 선택자와 일치하는 요소만 드래그 이벤트에 응답. css 선택자임에 유의. 예: '.title, .title > *', '#header', '.title *:not(input)'
dragHandleClass string 'drag-handle' 선택, 드래그 가능 콘텐츠에 적용되는 css 선택자에 매칭된 요소에 추가할 css 클래스명. css 선택자에 첫 번째로 매칭된 요소에 해당 css 클래스가 부여됨 기본 사용법
disabled boolean false 선택, 현재 요소의 드래그 가능 여부 제어. false면 가능, true면 불가 기본 사용법
enableDragFollow boolean false 선택, 실체 요소 따라가기 사용 여부(그림자 등 추가 이펙트 가능) 실체 요소 드래그 따라가기
dragFollowOptions {appendToBody?: boolean} -- 선택, 실체 드래그를 제어하는 설정 실체 요소 드래그 따라가기
dragFollowOptions.appendToBody boolean false 선택, 실체 드래그 클론 요소의 삽입 위치 제어. 기본 false는 소스 요소 부모의 모든 자식 중 마지막에 삽입, true면 부착. 설명 1 참고 실체 요소 드래그 따라가기
originPlaceholder {show?: boolean; tag?: string; style?: {cssProperties: string]: string}; text?: string; removeDelay?: number;} -- 선택, 원본 플레이스홀더 설정. 피드래그 요소의 원래 위치 자리 표시용 원본 플레이스홀더
originPlaceholder.show boolean true 선택, 표시 여부. 기본적으로 originPlaceholder에 Input이 있으면 표시, 특수한 경우 끌 수 있음
originPlaceholder.tag string 'div' 선택, tag 이름 사용. 기본적으로 originPlaceholder는 'div' 사용, 특수한 경우 교체 가능
originPlaceholder.style Object -- 선택, style 객체 전달. key는 스타일 속성, value는 스타일 값 원본 플레이스홀더
originPlaceholder.text string -- 선택, placeholder 내부 텍스트 원본 플레이스홀더
originPlaceholder.removeDelay number -- 선택, 원본 플레이스홀더를 드래그 후 지연 시간 후에 삭제. 애니메이션 구현에 용이, 단위 ms 원본 플레이스홀더

설명 1: dragFollowOptions의 appendToBody 사용 시나리오: 드래그한 요소가 떠난 뒤 원래 위치의 부모 객체가 소멸될 수 있다면, 클론이 소멸되지 않도록 body에 붙여야 해요. 기본적으로 스타일을 복사해 body로 클론된 실체의 스타일이 올바르게 유지되지만, DOM 노드 위치에 깊이 의존하는 일부 스타일과 속성은 제대로 적용되지 않을 수 있으니 일부 스타일은 직접 조정해야 해요.

dDraggable 이벤트
이벤트 타입 설명 Demo 링크
dragStartEvent EventEmitter<DragEvent> 드래그 시작의 DragStart 이벤트 기본 사용법
dragEndEvent EventEmitter<DragEvent> 드래그 종료의 DragEnd 이벤트 기본 사용법
dropEndEvent EventEmitter<DragEvent> 드롭 종료의 Drop 이벤트 기본 사용법

Drag DOM Events 자세히 보기: DragEvent

dDraggableBatchDrag 부가 디렉티브

사용 방법: dDraggableBatchDrag

dDraggableBatchDrag 속성
이름 타입 기본값 설명 Demo 링크
batchDragGroup | batchDrag string 'default' 선택, 일괄 드래그 그룹명. 서로 다른 그룹명
batchDragActive boolean false 선택, 요소를 일괄 드래그 그룹에 추가할지 여부. 설명 1 참고 일괄 드래그
batchDragLastOneAutoActiveEventKeys Array<key in DragEvent> ['ctrlKey'] 선택, 드래그로 일괄 선택 드래그 이벤트 판정 활성화 가능. 설명 2 참고
batchDragStyle Array<badge|stack> ['badge', 'stack'] 선택, 일괄 드래그 효과. badge는 오른쪽 위 통계 숫자, stack은 쌓임 효과를 의미하며, 배열에 해당 문자열이 있으면 유효 일괄 드래그

설명 1: batchDragActive가 true가 되면 요소가 그룹에 추가돼요. 추가 순서는 true가 된 순서 그대로이며, 먼저 추가된 요소가 배열 앞에 와요. 첫 번째 요소가 일괄 드래그 그룹 이름을 확정하고, 나중에 추가되는 요소의 그룹 이름이 앞선 그룹 이름과 일치하지 않으면 그룹에 추가될 수 없어요. 설명 2: batchDragLastOneAutoActiveEventKeys의 기본값은 ['ctrlKey']예요. ctrl 키를 누른 채 마지막 요소를 드래그하면 그 요소가 자동으로 일괄 드래그 그룹에 추가되는데, 판단 조건은 dragStart 이벤트의 ctrlKey가 true인 거예요. 현재는 true/false 판단만 지원해요. 이 파라미터는 배열이라서 어떤 속성 값이 하나라도 true이면 효과가 있으므로, 운영체제가 다를 때의 키 선언에 활용할 수 있어요.

dDraggableBatchDrag 이벤트
이름 타입 설명 Demo 링크
batchDragActiveEvent EventEmitter<{el: Element, data: any}> 드래그로 요소를 일괄 드래그 그룹에 추가하여, 외부에 해당 요소 선택을 알림 일괄 드래그

dDroppable 디렉티브

dDroppable 파라미터
파라미터 타입 기본값 설명 Demo 링크
dropScope string | Array<string> 'default' 선택, drop 영역 제한. dragScope에 대응 기본 사용법
dragOverClass string -- 선택, dragover 시 drop 요소에 적용할 css
placeholderStyle Object {backgroundColor: '#6A98E3', opacity: '.4'} 선택, sort 허용 시 자리 표시 표시용 원본 플레이스홀더
placeholderText string '' 선택, sort 허용 시 자리 표시 내부 텍스트용
allowDropOnItem boolean false 선택, sort 허용 시 요소 위로 드래그 허용. 트리 구조 드래그가 요소의 자식 노드가 될 수 있게 함 다층 트리 드래그
dragOverItemClass string -- 선택, allowDropOnItem이 true일 때만 유효. 요소 위로 드래그 허용 후 매칭된 요소에 스타일 추가 다층 트리 드래그
nestingTargetRect {height?: number, width?: number} -- 선택, 내장 리스트가 있을 때 부모 항목 높이가 늘어나는 문제 보정용. 여기서 height, width는 부모 항목 자체의 높이(세로 드래그용), 너비(가로 드래그용) 다층 트리 드래그
defaultDropPosition 'closest' | 'before' | 'after' 'closest' 선택, 드래그하여 배치 가능 영역(리스트 영역 밖)에 놓을 위치 설정. 'closest'는 가까운 곳에, 'before'는 리스트 앞에 추가, 'after'는 리스트 뒤에 추가 외부 배치 위치
dropSortCountSelector string -- 선택, sortable 컨테이너 정렬 시 개수를 셀 콘텐츠 선택자 이름. 개수에 포함되지 말아야 할 요소를 필터링하는 데 사용
dropSortVirtualScrollOption {totalLength?: number; startIndex?: number;} -- 선택, 가상 스크롤 리스트에서 올바른 dropIndex 반환용. totalLength는 리스트의 실제 전체 길이, startIndex는 현재 정렬 영역에 표시되는 첫 번째 dom의 리스트 내 index 값
switchWhileCrossEdge boolean false 선택, 건너뛰어 즉시 위치 교환 알고리즘 사용 여부. allowDropOnItem과 함께 사용 불가. allowDropOnItem이 true면 이 규칙은 무효
placeholderTag string 'div' 선택, 자리 표시 표시용 요소 태그
dDroppable 이벤트
이벤트 타입 설명 Demo 링크
dragEnterEvent EventEmitter<DragEvent> drag 요소 진입의 dragenter 이벤트 기본 사용법
dragOverEvent EventEmitter<DragEvent> drag 요소가 drop 영역 위에 있을 때의 dragover 이벤트 기본 사용법
dragLeaveEvent EventEmitter<DragEvent> drag 요소 이탈의 dragleave 이벤트 기본 사용법
dropEvent EventEmitter<DropEvent> 요소 하나를 드롭할 때 수신하는 이벤트. nativeEvent는 네이티브 drop 이벤트, 그 외는 정의 주석 참고 기본 사용법
DropEvent
type DropEvent = {
  nativeEvent: any; // 原生的drop事件
  dragData: any; // drag元素的dragData数据
  dropSubject: Subject<any>; //drop事件的Subject
  dropIndex?: number; // drop的位置在列表的index
  dragFromIndex?: number; // drag元素在原来的列表的index,注意使用虚拟滚动数据无效
  dropOnItem?: boolean; // 是否drop到了元素的上面,搭配allowDropOnItem使用
}

dSortable 디렉티브

정렬에 참여할 DOM 부모 컨테이너를 지정해요. (drop은 드래그할 수 있는 영역을 한정할 뿐이고, 실제 렌더링은 사용자가 직접 제어해요.)

dSortable 파라미터
이름 타입 기본값 설명 Demo 링크
dSortDirection 'v' | 'h' 'v' 'v' 세로 정렬, 'h' 가로 정렬
dSortableZMode boolean false z 모드 되돌림 정렬 여부. 설명 1 참고

설명 1: z 모드 정렬은 최종적으로 큰 방향을 기준으로 해요. 왼쪽에서 오른쪽으로 정렬하다가 행 끝에서 줄바꿈을 만난다면 세로 정렬 + z 모드를 사용해야 해요. 최종 데이터는 위에서 아래로 흐르고 일부 데이터만 왼쪽에서 오른쪽으로 흐르기 때문이에요.

dDropScrollEnhanced 파라미터
이름 타입 기본값 설명 Demo 링크
direction DropScrollDirection는 'v'|'h' 'v' 스크롤 방향. 세로 스크롤 'v', 가로 스크롤 'h' 드래그 스크롤 컨테이너 강화
responseEdgeWidth string | ((total: number) => string) '100px' 자동 스크롤 엣지 너비 응답. 함수의 경우 리스트 컨테이너의 같은 방향 상대 너비가 전달됨 드래그 스크롤 컨테이너 강화
speedFn DropScrollSpeedFunction 내장 함수 속도 함수. 비고 참고
minSpeed DropScrollSpeed는 number 50 최소 응답 속도. 함수 계산이 이 솪도보다 작으면 최소 속도 기준
maxSpeed DropScrollSpeed는 number 1000 최대 응답 속도. 함수 계산이 이 속도보다 크면 최대 속도 기준
viewOffset {forward?: DropScrollAreaOffset; backward?: DropScrollAreaOffset;} -- 드래그 영역 오프셋 설정. 일부 위치 보정용
dropScrollScope string| Array<string> -- 스크롤 트리거 허용 scope. 미설정 시 기본적으로 모든 scope 수신. 설정 시 draggable의 dragScope와 dropScrollScope가 일치해야 스크롤 트리거 드래그 스크롤 컨테이너 강화
backSpaceDroppable boolean true 스크롤 패널 위에서 동시에 스크롤 패널 아래의 배치 가능 요소로 드롭 트리거 허용 여부. 기본 true, false면 스크롤하며 드롭 불가

비고: speedFn 기본 함수는 (x: number) => Math.ceil((1 - x) * 18) * 100이에요. 전달되는 숫자 x는 마우스 위치가 가장자리에서 떨어진 거리가 전체 반응 폭에서 차지하는 비율이에요. 최종 속도는 speedFn(x)가 되지만, 최소 속도 minSpeed보다 작아지거나 최대 속도 maxSpeed보다 커지지는 않아요.

관련 타입 정의:

DropScrollDirection
export type DropScrollDirection = 'h' | 'v';
DropScrollSpeed
export type DropScrollEdgeDistancePercent = number; // unit: 1
export type DropScrollSpeed = number; // Unit: px/s
export type DropScrollSpeedFunction = (x: DropScrollEdgeDistancePercent) => DropScrollSpeed;
DropScrollAreaOffset
export type DropScrollAreaOffset = {
  left?: number;
  right?: number;
  top?: number;
  bottom?: number;
  widthOffset?: number;
  heightOffset?: number;
};

export enum DropScrollOrientation {
  forward, // Forward, right/bottom
  backward, // Backward, left/up
}
export type DropScrollTriggerEdge = 'left' | 'right' | 'top' | 'bottom';

DropScrollAreaOffset은 주요/보조 위치 지정 변에만 유효해요. forward는 오른쪽 또는 아래로 스크롤, backward는 왼쪽 또는 위로 스크롤을 의미해요.

direction v 위아래 스크롤 h 좌우 스크롤
forward는 아래 또는 오른쪽 left ,bottom top ,right
backward는 왼쪽 또는 위 left,top top,left
dDropScrollEnhancedSide 부속 디렉티브

두 방향 모두에서 스크롤이 필요하다면 dDropScrollEnhanced와 함께 dDropScrollEnhancedSide를 사용해야 해요. 파라미터 목록은 dDropScrollEnhanced 디렉티브와 같지만, 유일한 차이는 direction이에요. 'v'이면 side 부속 디렉티브의 실제 방향은 'h'가 돼요.

이름 타입 기본값 설명
direction DropScrollSpeed는 'v'|'h' 'v' 스크롤 방향. 세로 스크롤 'v', 가로 스크롤 'h'
responseEdgeWidth string | ((total: number) => string) '100px' 자동 스크롤 엣지 너비 응답. 함수의 경우 리스트 컨테이너의 같은 방향 상대 너비가 전달됨
speedFn DropScrollSpeedFunction 내장 함수 속도 함수. 비고 참고
minSpeed DropScrollSpeed는 number 50 최소 응답 속도. 함수 계산이 이 솪도보다 작으면 최소 속도 기준
maxSpeed DropScrollSpeed는 number 1000 최대 응답 속도. 함수 계산이 이 속도보다 크면 최대 속도 기준
viewOffset {forward?: DropScrollAreaOffset; backward?: DropScrollAreaOffset;} -- 드래그 영역 오프셋 설정. 일부 위치 보정용
dropScrollScope string| Array<string> -- 스크롤 트리거 허용 scope. 미설정 시 기본적으로 모든 scope 수신. 설정 시 draggable의 dragScope와 dropScrollScope가 일치해야 스크롤 트리거
backSpaceDroppable boolean true 스크롤 패널 위에서 동시에 스크롤 패널 아래의 배치 가능 요소로 드롭 트리거 허용 여부. 기본 true, false면 스크롤하며 드롭 불가

dDraggable & dDroppable 디렉티브 사용하기

<ul>
  <li v-dDraggable>Coffee</li>
  <li v-dDraggable>Tea</li>
  <li v-dDraggable>Milk</li>
</ul>
<div v-dDroppable>
  <p>Drop items here</p>
</div>

CSS

dDraggable & dDroppable 디렉티브는 모두 [dragOverClass]를 입력으로 받아요.
드래그와 드롭 시 hover 스타일을 제공하며, 문자열이라는 점에 주의하세요.

<div v-dDroppable="{
  dragOverClass: 'drag-target-border'
}">
  <p>Drop items here</p>
</div>

Drop 영역 제한하기

[dragScope]와 [dropScope]로 드래그 영역을 제한해요. 문자열이나 배열을 사용할 수 있고, drag와 drop 영역이 서로 대응될 때만 넣을 수 있어요.

<ul>
  <li v-dDraggable="{dragScope:'drink'}">Coffee</li>
  <li v-dDraggable="{dragScope:'drink'}">Tea</li>
  <li v-dDraggable="{dragScope:'meal'}">Biryani</li>
  <li v-dDraggable="{dragScope:'meal'}">Kebab</li>
  ...
</ul>
<div v-dDroppable="{
  dropScope: 'drink',
  dragOverClass: 'drag-target-border',
}">
  <p>只有 Drinks 可以放在这个container里</p>
</div>

<div v-dDroppable="{
  dropScope: ['drink', 'meal'],
  dragOverClass: 'drag-target-border'
}">
  <p>Meal 和 Drinks 可以放在这个container里</p>
</div>

데이터 전달하기

dDraggable은 dragData로 dDroppable에 데이터를 전달할 수 있어요.
dDroppable은 @dropEvent 이벤트로 데이터를 받아요.

<ul class="list-group">
  <li v-dDraggable="{ dragData: item }" v-for="item in items" class="list-group-item">{{item.name}}</li>
</ul>

<div class="panel panel-default" v-dDroppable="{'@dropEvent': (e) => onItemDrop(e)}">
  <div class="panel-heading">Drop Items here</div>
  <div class="panel-body">
    <li v-for="item in droppedItems" class="list-group-item">{{item.name}}</li>
  </div>
</div>
setup() {
  const items = [
    { name: 'Apple', type: 'fruit' },
    { name: 'Carrot', type: 'vegetable' },
    { name: 'Orange', type: 'fruit' },
  ];
  const droppedItems = [];

  onItemDrop(e) {
    // Get the dropped data here
    droppedItems.push(e.dragData);
  }
}
Drag Handle

Drag 핸들을 지정하면 draggable 자체가 아니라 실제로 draggable 이벤트에 반응할 요소를 정할 수 있어요.
이 파라미터는 반드시 문자열이어야 하고, 실제로는 css 선택자예요.

<li v-dDraggable="{
  dragHandle: '.drag-handle'
}">
  只有.drag-handle可以响应拖动事件来拖起li
  <div class="pull-right"><i class="drag-handle fa fa-bars fa-lg" aria-hidden="true"></i></div>
</li>
비동기 DropEnd로 Drag 요소에 알리기

dDraggable에는 dropEndEvent 이벤트가 있어요. 이 이벤트는 브라우저 기본 이벤트가 아니라 커스텀 이벤트이며, 컴포넌트가 자동으로 트리거하지 않아요. 트리거하는 방법은 dDroppable의 dropEvent 이벤트 파라미터에 담겨 오는 dropSubject를 이용하는 것으로, drag 요소의 dropEndEvent 이벤트를 발생시키고 싶을 때 dropSubject.next(params)를 호출해요. 보통 API 응답이 돌아온 뒤에 호출해요. 예를 들면:

<ul class="list-group">
  <li v-dDraggable="{
    dragData: item,
    '@dropEndEvent': e => dropEnd(e, i)
  }" v-for="(item, i) in items;">{{item.name}}</li>
</ul>

<div class="panel panel-default" v-dDroppable="{'@dropEvent': e => onItemDrop(e)}">
  <div class="panel-heading">Drop Items here</div>
  <div class="panel-body">
    <li v-for="item in droppedItems" class="list-group-item">{{item.name}}</li>
  </div>
</div>
setup() {
  onItemDrop(e) {
    ajax.onSuccess(() => {
      e.dropSubject.next(params); //此时才触发dragComponent的dropEnd 并且params对应onDropEnd的$event;
    });
  }
  onDropEnd(event, i) {}
}

협업 드래그: 2차원 드래그, 차원을 넘나드는 드래그 시나리오

협업 드래그 dDragSync

dDraggle 객체와 동시에 끌려 가는 객체들에 사용해요.

dDragSync 파라미터
파라미터 타입 기본값 설명 Demo 링크
dDragSync string '' 필수, 드래그 동기화 그룹명. 비었거나 빈 문자열이면 무효, 다른 콘텐츠와 동기화하지 않음 2D 드래그와 조합 드래그 미리보기
협업 드롭 dDropSortSync

dDroppable 객체와, droppable 내부의 sortable과 구조가 같은 sortable 영역에 사용해요. 주의: dDroppable 객체가 있는 쪽은 dDroppable과 동일한 객체에 dDropSortSync를 등록하고, dDroppable이 없는 다른 쪽은 정렬 영역에 배치해요.

dDropSortSync 파라미터
파라미터 타입 기본값 설명 Demo 링크
dDropSortSync string '' 필수, 드롭 동기화 그룹명. 비었거나 빈 문자열이면 무효, 다른 콘텐츠와 동기화하지 않음 2D 드래그와 조합 드래그 미리보기
dDropSyncDirection 'v'| 'h' 'v' 선택, dSortable 방향과 직교
협업 감시 박스 dDragDropSyncBox

dDragSync와 dDropSortSync의 공통 부모 조상을 집계하기 위해 사용해요. 파라미터는 없고, 공통 집계 영역에 배치하기만 하면 돼요.

드래그 미리보기: 드래그 미리보기를 교체해야 하는 시나리오

드래그 미리보기 dDragPreview

dDraggable과 함께 사용해야 하며, 드래그를 시작할 때 끌어 올려지는 객체의 템플릿으로 쓰여요.

dDragPreview 파라미터
파라미터 타입 기본값 설명 Demo 링크
dDragPreview TemplateRef<any> -- 필수, 미리보기 템플릿 참조 2D 드래그와 조합 드래그 미리보기
dragPreviewData any -- 선택, 커스텀 데이터. 템플트 변수로 획득
dragPreviewOptions { skipBatchPreview : boolean} -- 선택, 미리보기 옵션
dragPreviewOptions.skipBatchPreview boolean false 선택, 미리보기 옵션. 일괄 미리보기 스타일 처리 건너뛰기 여부. 일괄 드래그 미리보기 템플릿을 직접 처리하는 경우 건너뛰기 권장
dDragPreview 템플릿에서 사용할 수 있는 변수
변수 타입 변수 설명
data any 드래그 미리보기에서 전달된 dragPreviewData 데이터
draggedEl HTMLElement 드래그된 DOM 요소
dragData any 드래그 요소가 가진 dragData 데이터
batchDragData Array<any> 일괄 드래그 대상들의 dragData 배열. 피드래그 요소의 dragData를 포함하며 dragData가 첫 번째
dragSyncDOMElements Array<HTMLElement> 협동 드래그된 DOM 요소. draggedEl이 가리키는 DOM 요소는 제외
드래그 미리보기 보조 클론 노드 컴포넌트 <d-drag-preview-clone-dom-ref>

노드의 참조에서 DOM 클론 객체를 복원해 미리보기로 사용할 수 있어요.

파라미터 타입 기본값 설명 Demo 링크
domRef HTMLElement -- 필수(그렇지 않으면 무의미), 클론 노드의 DOM 참조 2D 드래그와 조합 드래그 미리보기
copyStyle boolean true 선택, 노드 클론 시 노드에 스타일 순차 클론 여부 2D 드래그와 조합 드래그 미리보기

더 알아보기 (Learn more)

dDraggable과 dDroppable 같은 디렉티브 조합만 익히면 기본 드래그부터 트리 중첩, 일괄 드래그, 2차원 협업 드래그까지 차근차근 확장할 수 있어요. 세부 파라미터와 이벤트는 본문의 API 표를 참고하세요.