ElementUI <el-dialog /> 弹窗可拖拽移动和拉伸大小

  1. 可拖拽移动到屏幕外(2.5.4 版本后支持overflow属性)。
  2. 可拖拽拉伸改变尺寸大小。
  3. 遮罩外元素可操作。
  4. 拖拽流畅不卡顿优化:
    4.1. 拖拽时遮罩无事件状态,避免触发弹窗外鼠标事件消耗性能;
    4.2. translate 移动,减少重绘;
    • 4.3 节流/防抖:减少频率 如 30~50ms;
    • 4.4 内容虚拟化:如果弹窗内容是长列表,可以虚拟滚动,减少 DOM 数量。

效果如下图:


980h8-n2pwk.gif

code:
App.vue:

<template>
  <el-button plain @click="dialogVisible = !dialogVisible">
    Click to open the Dialog
  </el-button>
  <div>
    dragging: {{dragging}}
  </div>



  <el-dialog
    v-model="dialogVisible"
    title="Title"
    width="500px"
    custom-class="my-dialog"
    append-to-body
    :close-on-click-modal="false"
  >
    <div 
    class="content-wrapper" 
    :class="{ dragging }"
    v-drag-resize="{
        initHeight: 300,
        initWidth: 500,
        updateDraggingState
    }">
      <span>The dialog content</span>
    </div>
  </el-dialog>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import { vDragResize } from './vDragResize';
const dialogVisible = ref(false)

let dragging = ref(false)
const updateDraggingState = async (bool: boolean) => {
  dragging.value = bool
  console.log("dragging.value: ", dragging.value)
}

</script>
<style  >
.el-dialog__header {
  border: 1px solid #666;
  border-radius: 5px;
}

.el-overlay-dialog:has(.el-dialog.my-dialog){
  overflow: hidden;
  padding: 0;
}

.el-dialog.my-dialog {
  margin: 0; /** 因为是 translate 移动,所以不需 margin */
}

/** 不拖拽时,遮罩外可操作: */
div:has(.el-overlay-dialog .el-dialog.my-dialog .content-wrapper:not(.dragging)) {
  pointer-events: none; 
  background-color: transparent;
}

.el-dialog.my-dialog {
  pointer-events: auto;
  outline: 1px solid #494949;
}
</style>

vDragResize.ts:

/**
 * ElDialog 拖拽移动和拉伸调整大小
 * @example
 * v-drag-resize='{initHeight: 500}'
 */

import type { Directive, DirectiveBinding } from 'vue'

interface BindingOption {
  initHeight: number // 拖拽拉伸盒子的初始高度
  initWidth: number
  updateDraggingState?: Function // 更新是否拖拽中状态
}

interface ElType extends HTMLElement {
  setCursor: (e: MouseEvent) => void
  stretchMousedown: (e: MouseEvent) => void
}

type Points = {
  left: number // 盒子距屏幕位移
  top: number
  width: number // 盒子宽高
  height: number
  x: number // 鼠标按下坐标
  y: number
  endX?: number // 鼠标移动坐标
  endY?: number
}

/**
 * 拉伸触控间距
 */
const OFFSET = 8

/**
 * 鼠标悬停样式
 */
const CURSOR_TYPE = {
  top: 'n-resize',
  bottom: 's-resize',
  left: 'w-resize',
  right: 'e-resize',
  right_top: 'ne-resize',
  left_top: 'nw-resize',
  left_bottom: 'sw-resize',
  right_bottom: 'se-resize',
  default: 'default',
  // move: 'move'
}

/**
 * 判断鼠标悬浮指针方向
 * @param points
 */
const checkCursorType = (points: Points) => {
  const { left, top, width, height, x, y } = points

  const inRight = x > left + width - OFFSET
  const inLeft = left + OFFSET > x
  const inBottom = y > top + height - OFFSET
  const inTop = top + OFFSET > y

  let type: keyof typeof CURSOR_TYPE

  if (inRight && y <= top + height - OFFSET && top + OFFSET <= y) {
    type = 'right'
  } else if (inLeft && y <= top + height - OFFSET && top + OFFSET <= y) {
    type = 'left'
  } else if (inBottom && x <= left + width - OFFSET && left + OFFSET <= x) {
    type = 'bottom'
  } else if (inTop && x <= left + width - OFFSET && left + OFFSET <= x) {
    type = 'top'
  } else if (inRight && inBottom) {
    type = 'right_bottom'
  } else if (inLeft && inBottom) {
    type = 'left_bottom'
  } else if (inTop && inRight) {
    type = 'right_top'
  } else if (inTop && inLeft) {
    type = 'left_top'
  }
  return type || 'default'
}

/**
 * 设置文本滑选
 * @param value
 */
const setUserSelect = (value: 'none' | '') => {
  document.body.style.userSelect = value
}

/**
 * 判断是否弹窗头部可拖拽区域
 * @param points
 * @param headerHeight
 */
const checkHeaderDragZoom = (points: Points, headerHeight: number) => {
  const { left, top, width, x, y } = points
  if (
    x > left + OFFSET &&
    x < left + width - OFFSET &&
    y > top + OFFSET &&
    y < top + headerHeight
  ) {
    return true
  }
  return false
}

/**
 * 边界/宽高限制
 * @param points
 * @param boxEl
 */
const boundaryLimit = (points: Points, boxEl: ElType, translateX = 0, translateY = 0,screenWidth:number, screenHeight:number) => {
  const { left, top, width, height, x, y, endX = 0, endY = 0 } = points
  const diffX = endX - x
  const diffY = endY - y
  const type = checkCursorType({ left, top, width, height, x, y })
  const directions = type.split('_') // 转换为数组, 方便判断

  if (directions.includes('right')) {
    if (width + diffX > screenWidth - left) {
      boxEl.style.width = screenWidth - left + 'px'
    } else {
      boxEl.style.width = width + diffX + 'px'
    }
  }
  let translateX_ = translateX
  let translateY_ = translateY
  if (directions.includes('left')) {
    if (width - diffX > width + left) {
      boxEl.style.width = width + left + 'px'
      translateX_ = 0
    } else {
      boxEl.style.width = width - diffX + 'px'
      translateX_ = left + diffX
    }
  }
  if (directions.includes('top')) {
    if (height - diffY > height + top) {
      boxEl.style.height = height + top + 'px'
      translateY_ = 0
    } else {
      boxEl.style.height = height - diffY + 'px'
      translateY_ = top + diffY
    }
  }
  if (directions.includes('bottom')) {
    if (height + diffY > screenHeight - top) {
      boxEl.style.height = screenHeight - top + 'px'
    } else {
      boxEl.style.height = height + diffY + 'px'
    }
  }

  boxEl.style.transform = `translate3d(${translateX_}px, ${translateY_}px, 0)`
}

/**
 * 查找弹窗内容最外层,作为拉伸盒子
 * @param el
 * @returns
 */
const findResizeBox = (el: HTMLElement) => {
  if (el.classList.contains('el-dialog')) return el
  if (el.parentElement) return findResizeBox(el.parentElement)
}

const getTranslate = (resizeBox: HTMLElement) => {
  const rs = getComputedStyle(resizeBox).transform.split(',')
  return {
    translateX: parseFloat(rs[4]),
    translateY: parseFloat(rs[5]),
  }
}

const screenWidth =  document.body.clientWidth || document.documentElement.clientWidth || 600 
const screenHeight =  document.body.clientHeight || document.documentElement.clientHeight || 800
 

export const vDragResize: Directive = {
  mounted: (el: HTMLElement, binding: DirectiveBinding<BindingOption>) => {
    // 拖拽拉伸大小盒子
    const boxEl = findResizeBox(el) as ElType
    // 拖拽移动手柄
    const headerEl = boxEl?.querySelector('.el-dialog__header') as ElType

    if (!headerEl || !boxEl) return

    boxEl.style.height = binding.value.initHeight + 'px'
    const translateX = (screenWidth - binding.value.initWidth) / 2
    const translateY = (screenHeight - binding.value.initHeight) / 2
    boxEl.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`

    // dragStart
    const dragStart = () => {
      setUserSelect('none')
      binding.value?.updateDraggingState?.(true)
    }

    // dragEnd
    const dragEnd = () => {
      setUserSelect('')
      binding.value?.updateDraggingState?.(false)
    }

    // 设置鼠标悬停样式
    const setCursor = (e: MouseEvent) => {
      const x = e.clientX
      const y = e.clientY
      const { left, top, width, height } = boxEl.getBoundingClientRect()
      const headerHeight = headerEl.clientHeight
      if (checkHeaderDragZoom({ left, top, width, height, x, y }, headerHeight)) {
        boxEl.style.cursor = 'move'
      } else {
        let type = checkCursorType({ left, top, width, height, x, y })
        boxEl.style.cursor = CURSOR_TYPE[type] || 'default'
      }
    }

    let initX: number
    let initY: number
    // 拖拽移动-鼠标按下
    const dragMousedown = (e: MouseEvent) => {
      const { translateX, translateY } = getTranslate(boxEl)
      initX = e.clientX - translateX
      initY = e.clientY - translateY

      // 移动开始
      const dragMousemove = function (e: MouseEvent) {
        e.preventDefault()

        let x = e.clientX - initX
        let y = e.clientY - initY

        // 边界限制,避免拖拽手柄完全移出屏幕
        const minX = -initX
        const maxX = screenWidth
        const maxY = screenHeight - 40
        const minY = 0

        if (x < minX) {
          x = minX
        } else if (x > maxX) {
          x = maxX
        }

        if (y < minY) {
          y = minY
        } else if (y > maxY) {
          y = maxY
        }

        boxEl.style.transform = `translate3d(${x}px, ${y}px, 0)`
      }

      // 移动结束
      const dragMouseup = function () {
        document.removeEventListener('mousemove', dragMousemove)
        document.removeEventListener('mouseup', dragMouseup)
        dragEnd()
      }

      document.addEventListener('mousemove', dragMousemove)
      document.addEventListener('mouseup', dragMouseup)
    }

    // 拖拽拉伸-鼠标按下
    const stretchMousedown = (e: MouseEvent) => {
      const x = e.clientX
      const y = e.clientY
      const { left, top, width, height } = boxEl.getBoundingClientRect()
      const { translateX, translateY } = getTranslate(boxEl)
      dragStart()

      // header
      const headerHeight = headerEl.clientHeight
      const headerRange = checkHeaderDragZoom({ left, top, width, height, x, y }, headerHeight)
      if (headerRange) {
        dragMousedown(e)
        return
      }

      // 拉伸开始
      const stretchMousemove = function (e: MouseEvent) {
        e.preventDefault()

        const endX = e.clientX
        const endY = e.clientY
        boundaryLimit(
          {
            left,
            top,
            width,
            height,
            x,
            y,
            endX,
            endY,
          },
          boxEl,
          translateX,
          translateY,
          screenWidth,
          screenHeight
        )
      }

      // 拉伸结束
      const stretchMouseup = function () {
        document.removeEventListener('mousemove', stretchMousemove)
        document.removeEventListener('mouseup', stretchMouseup)
        dragEnd()
      }

      document.addEventListener('mousemove', stretchMousemove)
      document.addEventListener('mouseup', stretchMouseup)
    }

    boxEl.addEventListener('mousemove', setCursor)
    boxEl.addEventListener('mousedown', stretchMousedown)
    boxEl.setCursor = setCursor
    boxEl.stretchMousedown = stretchMousedown
  },

  beforeUnmount: function (el: HTMLElement) {
    // 拖拽拉伸大小盒子
    const boxEl = findResizeBox(el) as ElType
    if (!boxEl) return

    const setCursor = boxEl.setCursor
    const stretchMousedown = boxEl.stretchMousedown

    boxEl.removeEventListener('mousemove', setCursor)
    boxEl.removeEventListener('mousemove', stretchMousedown)
    // 删除保存在 el 中的函数
    boxEl.setCursor = null
    boxEl.stretchMousedown = null
  },
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容