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 표를 참고하세요.