Skip to content

Drag and Drop

Drag and Drop Scaling A drag-and-drop component that works on both mobile and PC, consisting of two components: Draggable and Droppable.

  • Draggable: Represents a draggable element.
  • Droppable: Represents an area where draggable elements can be dropped.

To accommodate various use cases:

  • Draggable can configure its drag type via type.
  • Droppable can configure allowed drag types via acceptDragType.
  • Droppable only accepts Draggable elements whose type is included in its acceptDragType.

Installation

INFO

If you need other components, refer toFull Components Installation

To install this component individually, run:

shell
npm install @havue/drag-and-drop --save
shell
yarn add @havue/drag-and-drop
shell
pnpm install @havue/drag-and-drop

Import

vue
<script>
import { HvDraggable, HvDroppable } from 'havue'
// or 
import { HvDraggable, HvDroppable } from '@havue/components'
// or
import { HvDraggable, HvDroppable } from '@havue/drag-and-drop'
</script>

Example

  • Left-side label blocks are Draggable elements, displaying their type and size.
  • The ​gray Draggable element has immediate="right", allowing instant dragging to the right.
  • ​Right-side contains two Droppable areas:
    • Green labels can only be dropped in the green area.
    • Yellow labels can only be dropped in the yellow area.

下列卡片可拖动:

yellow: 195 * 65
immediate: right
yellow: 177 * 55
green: 190 * 63
immediate: right
yellow: 197 * 68
yellow: 165 * 68
immediate: right
yellow: 167 * 83
yellow: 161 * 92
immediate: right
green: 190 * 91
green: 182 * 78
immediate: right
yellow: 165 * 58
green: 186 * 65
immediate: right
yellow: 169 * 53
yellow: 169 * 99
immediate: right
yellow: 161 * 75
green: 150 * 54
immediate: right
green: 170 * 58
yellow: 169 * 74
immediate: right
green: 198 * 70
green: 158 * 98
immediate: right
green: 198 * 72
green: 198 * 77
immediate: right
yellow: 151 * 82
yellow: 187 * 91
immediate: right
yellow: 191 * 93
yellow: 161 * 59
immediate: right
green: 188 * 56
green: 160 * 95
immediate: right
yellow: 153 * 64
green: 160 * 78
immediate: right
green: 200 * 77
yellow: 185 * 81
immediate: right
yellow: 197 * 71
yellow: 171 * 81
immediate: right
green: 158 * 66
yellow: 185 * 62
immediate: right
green: 166 * 94
green: 182 * 81
immediate: right
yellow: 173 * 69
green: 174 * 55
immediate: right
green: 160 * 99

下方为可拖放区域green:

下方为可拖放区域yellow:

Click to view code
vue
<template>
  <div class="dnd-demo" ref="divRef">
    <div class="left-box">
      <p>下列卡片可拖动:</p>
      <div class="top-drag-list-box">
        <Draggable :type="item.type" v-for="(item, i) in DragItems" :data="item" :key="i" :immediate="item.immediate">
          <div class="drag-box" :style="item.style">
            {{ item.type }}: {{ `${item.width} * ${item.height}` }}
            <br />
            {{ item.immediate ? 'immediate: ' + item.immediate : '' }}
          </div>
        </Draggable>
      </div>
    </div>

    <div class="right-box">
      <p>下方为可拖放区域green:</p>
      <Droppable
        accept-drag-type="green"
        @enter="
          (type: DragAndDropDragType, point: DragAndDropPoint, data: any) => {
            enteredType = 'green'
            onEnter(type, point, data)
          }
        "
        @move="onMove"
        @leave="onLeave"
        @drop="onDrop"
      >
        <div class="drop-box">
          <div class="preview-box" v-if="enteredType === 'green'" :style="previewStyle"></div>
          <div
            class="inner-box-item"
            v-for="item in greenInnerBoxList"
            :key="item.key"
            :style="{
              top: item.y,
              left: item.x,
              width: item.width,
              height: item.height
            }"
          ></div>
        </div>
      </Droppable>

      <p>下方为可拖放区域yellow:</p>
      <Droppable
        accept-drag-type="yellow"
        @enter="
          (type: DragAndDropDragType, point: DragAndDropPoint, data: any) => {
            enteredType = 'yellow'
            onEnter(type, point, data)
          }
        "
        @move="onMove"
        @leave="onLeave"
        @drop="onDrop"
      >
        <div class="drop-box yellow">
          <div class="preview-box" v-if="enteredType === 'yellow'" :style="previewStyle"></div>
          <div
            class="inner-box-item"
            v-for="item in yellowInnerBoxList"
            :key="item.key"
            :style="{
              top: item.y,
              left: item.x,
              width: item.width,
              height: item.height
            }"
          ></div>
        </div>
      </Droppable>
    </div>
  </div>
</template>
ts
<script setup lang="ts">
import type { DragAndDropPoint, DragAndDropDragType } from '@havue/drag-and-drop'
import { computed, ref, reactive } from 'vue'
// import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/drag-and-drop'
import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/components'

type BoxType = {
  key: string | number | symbol
  x: string
  y: string
  width: string
  height: string
}

type EnteredType = 'green' | 'yellow' | ''

const divRef = ref<HTMLElement>()

const enteredType = ref<EnteredType>('')
const enteredPoint = ref({
  x: 0,
  y: 0
})
const previewData = ref()

const DragItems = reactive(
  Array(40)
    .fill(0)
    .map((_, i) => {
      let random1 = Math.round(Math.random() * 50)
      let random2 = Math.round(Math.random() * 50)
      return {
        type: random1 % 2 === 0 ? 'green' : 'yellow',
        width: random1 + 150,
        height: random2 + 50,
        immediate: i % 2 == 0 ? ('right' as const) : undefined,
        style: {
          background: i % 2 == 0 ? 'gray' : ''
        }
      }
    })
)

const greenInnerBoxList = reactive<BoxType[]>([
  {
    key: 1,
    x: '20px',
    y: '20px',
    width: '20px',
    height: '20px'
  },
  {
    key: 2,
    x: '100px',
    y: '30px',
    width: '50px',
    height: '200px'
  },
  {
    key: 3,
    x: '200px',
    y: '40px',
    width: '90px',
    height: '40px'
  }
])

const yellowInnerBoxList = reactive<BoxType[]>([
  {
    key: 4,
    x: '200px',
    y: '40px',
    width: '90px',
    height: '40px'
  }
])

const previewStyle = computed(() => {
  const preview = previewData.value || {}
  return {
    top: `${enteredPoint.value.y}px`,
    left: `${enteredPoint.value.x}px`,
    width: `${preview.width || 100}px`,
    height: `${preview.height || 50}px`
  }
})

function onEnter(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
  enteredPoint.value = point
  previewData.value = data
}

function onMove(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
  enteredPoint.value = point
  previewData.value = data
}

function onLeave() {
  enteredType.value = ''
  enteredPoint.value = { x: 0, y: 0 }
  previewData.value = undefined
}

function onDrop(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
  data = data || {}
  const { width = 100, height = 50 } = data
  if (enteredType.value === 'green') {
    greenInnerBoxList.push({
      key: Date.now(),
      x: `${point.x - width / 2}px`,
      y: `${point.y - height / 2}px`,
      width: `${width}px`,
      height: `${height}px`
    })
  } else if (enteredType.value === 'yellow') {
    yellowInnerBoxList.push({
      key: Date.now(),
      x: `${point.x - width / 2}px`,
      y: `${point.y - height / 2}px`,
      width: `${width}px`,
      height: `${height}px`
    })
  }
  enteredType.value = ''
  enteredPoint.value = { x: 0, y: 0 }
  previewData.value = undefined
}
</script>
scss
<style lang="scss" scoped>
.dnd-demo {
  display: flex;
  width: 100%;
  height: 700px;
  overflow: auto;
  background-color: rgb(20 177 177 / 41.6%);

  p {
    margin: 5px;
  }

  .left-box {
    width: 30%;
    height: 100%;
    margin-right: 15px;
    overflow: auto;
    background-color: rgb(162 101 22 / 69.4%);

    .top-drag-list-box {
      display: flex;
      flex-wrap: wrap;
      gap: 20px;
      justify-content: flex-start;
      padding: 10px;

      .drag-box {
        width: 130px;
        height: 50px;
        color: white;
        user-select: none;
        background-color: black;
      }
    }
  }

  .right-box {
    flex: 1;

    .drop-box {
      position: relative;
      width: 400px;
      height: 300px;
      overflow: hidden;
      background-color: rgb(11 104 28);

      &.yellow {
        background-color: rgb(188 149 21);
      }

      .preview-box {
        position: absolute;
        background-color: rgb(127 255 212 / 30%);
        transform: translate(-50%, -50%);
      }

      .inner-box-item {
        position: absolute;
        background-color: rgb(143 10 65 / 46.6%);
      }
    }
  }
}
</style>

Draggable Props

PropertyDescriptionTypeDefault
typeDrag element typeDragAndDropDragType-
immediate?Direction where the element responds to drag immediately (default requires long-press)ImmediateType | ImmediateType[]-
disabled?Whether the element is disabledboolean-
data?Data associated with the draggable element (passed to Droppable)any-
ts
type DragAndDropDragType: string | number | symbol

type ImmediateType = 'left' | 'right' | 'top' | 'bottom' | 'all' | undefined

Draggable Slots

Slot NameDescription
defaultDefault content
drag-itemElement displayed during dragging

Droppable Props

PropertyDescriptionTypeDefault
acceptDragTypeAccepted drag typesDragAndDropDragType | Array<DragAndDropDragType>-

Droppable Events

EventDescriptionParameter Types
enterTriggered when Draggable enters the area(type: DragType, point: DragAndDropPoint, data: any) => void
moveTriggered while Draggable moves within the area(type: DragType, point: DragAndDropPoint, data: any) => void
dropTriggered when Draggable is dropped(type: DragType, point: DragAndDropPoint, data: any) => void
leaveTriggered when Draggable leaves the area(type: DragType, data: any) => void
ts
DragAndDropPoint: {
  /** X-coordinate within Droppable */
  x: string
  /** Y-coordinate within Droppable */
  y: string
}

Droppable Slots

Slot NameDescription
defaultDefault content