vue 3.0 拖拽组件

拖拽容器

12af53b2-4f10-48f0-85c4-061e86225d47.gif

使用


// html
<Move :data='initSize' @update='update' >
  <div :style='style'></div>
</Move>

// ts
import { Move, MoveBlock } from './components/move'

export default defineComponent({
  components: {
    Move
  },
  setup(){
    const intiSize = ref({
      width: 100,
      heiht: 100
    })

    const style = computed(() => {
      const { width, height } = initSize.value
      return {
        width: `${width}px`,
        height: `${height}px`
      }
    })

    const update = (d: MoveBlock) => {
      initSiz.value = {
        width: d.width,
        height: d.height
      }
    }
    
    return {
      intiSize,
      style,
      update
    }
  }
})

参数

名称 类型 默认值 说明
unit string px 单位
data Obejct { width: 100, height: 100, top: 0, bottom: 0, left: 0, right: 0 } 初始位置及尺寸

事件

名称 说明 参数 备注
update 拖拽更新数据 { width, height, top, bottom, left, right }

move hooks

MovePoint 拖拽点定义


type MovePoint = 'topLeft'
  | 'topRight'
  | 'middleLeft'
  | 'middleRight'
  | 'bottomLeft'
  | 'bottomRight'
  | 'middleTop'
  | 'middleBottom'

MoveBlock 容器参数

export interface MoveBlock {
  [s: string]: number
}

点位坐标

export interface Position {
  x: number
  y: number
}

StartState 鼠标触发时的初始状态

export interface StartState {
  preMoveBlock: MoveBlock
  type: MovePoint | ''
  startX: number
  startY: number
}

拖拽点事件函数

根据初始点位信息计算新坐标位置

export interface MoveEvent {
  (diff: Position, start: StartState): MoveBlock
}

useMovePoint 拖拽点逻辑

封装各个拖拽点计算方法

参数

名称 类型 默认值 说明
ctx SetupContext 上下文环境
updateBlock fn(d: MoveBlock):void 点位移动时触发更新函数

周期事件

名称 参数 说明
pointMouseDown startState 鼠标键按下, 返回初始状态
pointMouseMove diff 鼠标移动, 返回计算后的插值
pointMouseUp 鼠标键抬起

返回

名称 类型 说明
startState StartState 鼠标按下时的初始状态
onPointMousedown (e: MouseEvent, preMoveBlock: MoveBlock, t: MovePoint):void 鼠标按下后,触发拖拽监听

useMoveBlock 拖拽容器逻辑

封装拖拽容易移动计算方法

参数

名称 类型 默认值 说明
ctx SetupContext 上下文环境

周期事件

名称 参数 说明
blockMouseDown moveBlock 鼠标键按下事件 容器初始状态
blockMouseMove moveBlock 鼠标移动事件,容器状态
blockMouseDown { startX, startY } 鼠标键抬起事件, 当前鼠标位置

返回

名称 类型 默认值 说明
unit Ref<string> px 单位
moveBlock Ref<moveBlock> 当前容器状态
moveBlockStyle Computed<moveBlock> 容器计算样式
updateBlock fn(d: MoveBlock) 容器状态更新
mouseMoveLock fn(d: MovewBlock): MovewBlock 尺寸边界计算
onMouseDown fn(e: MouseEvent) 拖拽监听触发

源码

move.vue

<style lang='stylus' scoped>
$color = orange
.move{
  position absolute
  border 1px dashed transparent
  $pointSize=10px
  &:hover{
    border-color $color
    .move-point{
      opacity 1
    }
  }
  &-point{
    opacity 0
    position absolute
    width $pointSize
    height $pointSize
    border-radius $pointSize
    border 1px solid $color
    background white
    transition all .3s
    z-index: 1
    &:hover{
      transform scale(1.5, 1.5)
    }
  }
  $position = $pointSize/2 * -1
  .topLeft{
    top $position
    left $position
    cursor nw-resize
  }
  .topRight{
    top $position
    right $position
    cursor ne-resize
  }
  .middleTop{
    top $position
    left 0
    right 0
    margin auto
    cursor n-resize
  }
  .middleBottom{
    bottom $position
    left 0
    right 0
    margin auto
    cursor s-resize
    
  }
  .middleLeft{
    top 0
    bottom 0
    left $position
    margin auto
    cursor w-resize
  }
  .middleRight{
    top 0
    bottom 0
    right $position
    margin auto
    cursor e-resize
  }
  .bottomLeft{
    left $position
    bottom $position
    cursor sw-resize
  }
  .bottomRight{
    right $position
    bottom $position
    cursor se-resize
  }
}
</style>
<template>
  <div class='move' :style='moveBlockStyle' @mousedown.stop='onMouseDown' >
    <slot></slot>
      <div
      class='move-point'
      v-for='type of movePoints'
      @mousedown.stop='e => onPointMousedown(e, moveBlock, type)'
      :class='type'
      :key='type'>
      </div>
  </div>
</template>
<script lang='ts'>
import { defineComponent, PropType, watch } from 'vue'
import {
  useMoveBlock,
  useMovePoint,
  movePoints,
  MoveBlock
} from './hooks'

export default defineComponent({
  props: {
    unit: {
      type: String,
      default: 'px'
    },
    data: {
      type: Object as PropType<MoveBlock>,
      default: () => ({
        width: 100,
        height: 100,
        top: 0,
        bottom: 0,
        left: 0,
        right: 0
      })
    }
  },
  setup(props, ctx){
    const {
      unit,
      moveBlock,
      moveBlockStyle,
      updateBlock,
      mouseMoveLock,
      onMouseDown
    } = useMoveBlock(ctx)
    
    const update = (d: MoveBlock) => {
      updateBlock(mouseMoveLock(d))
    }
    const {
      onPointMousedown
    } = useMovePoint(ctx, update)

    watch(() => props.unit, () => {
      unit.value = props.unit
    }, { immediate: true })
    
    updateBlock(props.data)
    
    return {
      movePoints,
      moveBlockStyle,
      moveBlock,
      onPointMousedown,
      onMouseDown
    }
  }
})
</script>

hooks

import { ref, computed, SetupContext } from 'vue'

type MovePoint = 'topLeft'
  | 'topRight'
  | 'middleLeft'
  | 'middleRight'
  | 'bottomLeft'
  | 'bottomRight'
  | 'middleTop'
  | 'middleBottom'

export const movePoints: MovePoint[] = [
  'topLeft',
  'topRight',
  'bottomLeft',
  'bottomRight',
  'middleTop',
  'middleBottom',
  'middleLeft',
  'middleRight',
]

export interface MoveBlock {
  [s: string]: number
}

export interface Position {
  x: number
  y: number
}

export interface StartState {
  preMoveBlock: MoveBlock
  type: MovePoint | ''
  startX: number
  startY: number
}

export interface MoveEvent {
  (diff: Position, start: StartState): MoveBlock
}

export const topLeft: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    width: preMoveBlock.width - diff.x,
    height: preMoveBlock.height - diff.y,
    top: preMoveBlock.top + diff.y,
    left: preMoveBlock.left + diff.x,
  }
}

export const topRight: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    width: preMoveBlock.width + diff.x,
    height: preMoveBlock.height - diff.y,
    top: preMoveBlock.top + diff.y,
  }
}

export const middleTop: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    height: preMoveBlock.height - diff.y,
    top: preMoveBlock.top + diff.y,
  }
}

export const middleBottom: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    height: preMoveBlock.height + diff.y
  }
}

export const middleLeft: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    width: preMoveBlock.width - diff.x,
    left: preMoveBlock.left + diff.x,
  }
}

export const middleRight: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    width: preMoveBlock.width + diff.x,
  }
}

export const bottomLeft: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    width: preMoveBlock.width - diff.x,
    height: preMoveBlock.height + diff.y,
    left: preMoveBlock.left + diff.x,
  }
}

export const bottomRight: MoveEvent = (diff: Position, start: StartState) => {
  const { preMoveBlock } = start
  return {
    width: preMoveBlock.width + diff.x,
    height: preMoveBlock.height + diff.y
  }
}


export function useMovePoint(ctx: SetupContext, updateBlock: (d: MoveBlock) => void) {
  const eventMap: { [s: string]: MoveEvent } = {
    topLeft,
    topRight,
    bottomLeft,
    bottomRight,
    middleTop,
    middleBottom,
    middleLeft,
    middleRight,
  }

  const startState = ref<StartState>({
    preMoveBlock: {},
    type: '',
    startX: 0,
    startY: 0
  })

  const onPointMousedown = (e: MouseEvent, preMoveBlock: MoveBlock, t: MovePoint) => {
    startState.value = {
      type: t,
      preMoveBlock: { ...preMoveBlock },
      startX: e.clientX,
      startY: e.clientY
    }

    const cb = (event: MouseEvent) => {
      const { startX, startY, type } = startState.value
      const diff = {
        x: event.clientX - startX,
        y: event.clientY - startY
      }
      const setFn = eventMap[type]
      if (setFn) {
        updateBlock(setFn(diff, startState.value))
      }
      ctx.emit('pointMouseMove', { ...diff })
    }
    ctx.emit('pointMouseDown', { ...startState.value })
    document.addEventListener('mousemove', cb)
    document.addEventListener('mouseup', () => {
      document.removeEventListener('mousemove', cb)
      ctx.emit('pointMouseUp')
    })
  }

  return {
    startState,
    onPointMousedown
  }
}

export function useMoveBlock(ctx: SetupContext) {
  const unit = ref('px')
  const moveBlock = ref<MoveBlock>({
    width: 0,
    height: 0,
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  })

  const updateBlock = (data: MoveBlock) => {
    const _data = { ...moveBlock.value, ...data }
    if (_data.width <= 0) {
      _data.width = 0
    }
    if (_data.height <= 0) {
      _data.height = 0
    }
    moveBlock.value = _data
    ctx.emit('update', { ...moveBlock.value })
  }

  // 防止尺寸为零时,元素移动
  const mouseMoveLock = (d: MoveBlock) => {
    const _d = { ...d }
    if (_d.width <= 0) {
      _d.width = 0
      _d.left = moveBlock.value.left
    }

    if (_d.height <= 0) {
      _d.height = 0
      _d.top = moveBlock.value.top
    }
    return _d
  }
  const moveBlockStyle = computed(() => {
    return Object.entries(moveBlock.value).reduce((acc, [key, val]) => {
      return { ...acc, [key]: `${val}${unit.value}` }
    }, {})
  })

  const prePosition = ref({
    startX: 0,
    startY: 0
  })

  const onMouseMove = (e: MouseEvent) => {
    const { startX, startY } = prePosition.value
    const diff = {
      x: e.clientX - startX,
      y: e.clientY - startY
    }
    prePosition.value = {
      startX: e.clientX,
      startY: e.clientY
    }
    const { top, left } = moveBlock.value

    updateBlock({
      top: top + diff.y,
      left: left + diff.x
    })
    ctx.emit('blockMouseMove', moveBlock.value)
  }

  const onMouseUp = () => {
    ctx.emit('blockMouseUp')
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('moouseup', onMouseUp)
  }

  const onMouseDown = (e: MouseEvent) => {
    prePosition.value = {
      startX: e.clientX,
      startY: e.clientY
    }
    ctx.emit('blockMouseDown', { ...prePosition.value })
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }

  return {
    unit,
    moveBlock,
    moveBlockStyle,
    updateBlock,
    mouseMoveLock,
    onMouseDown
  }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容