Vue 实现可拖拽弹窗

一、实现原理

1、获取鼠标在div中的位置
2、设置 div 的 left 和 top 使其跟随鼠标位置移动,达到拖拽的效果

二、实现步骤

1、UI
<template>
  <div v-if="visible" class="my_dialog">
    <!-- 遮罩层 -->
    <div class="my_dialog_mask"></div>
    <div class="my_dialog_box" id="my_dialog_box" v-drag>
      <!-- 标题 -->
      <div class="my_dialog_title">
        {{title}}
        <span class="my_dialog_close" @click="cancel">X</span>
      </div>
      <!-- 内容 -->
      <div class="my_dialog_content">
        <slot></slot>
      </div>
      <!-- 底部按钮 -->
      <div class="my_dialog_bottom">
        <button class="btn cancelBtn" v-if="showCancelButton" @click="cancel">{{canceltext}}</button>
        <button class="btn confirmBtn" @click="confirm">{{confirmtext}}</button>
      </div>
    </div>
  </div>
</template>
2、组件定义props

visible:控制弹窗显示,false 不显示,true 显示
title:弹窗标题,默认 提示
confirmtext:确认按钮文案,默认 确定
canceltext: 取消按钮文案,默认 取消
showCancelButton:是否显示取消按钮,false 否,true 是,默认 true

3、组件事件回调 $emit
methods: {
    cancel: function () {
      // .sync 实现弹窗显示 or 隐藏
      this.$emit("update:visible", false)
      this.$emit("cancel")
    },
    confirm: function () {
      this.$emit("confirm")
    },
  },
4、组件使用到的相关属性
  • event.clientX / event.clientY:鼠标触发时指针相对于浏览器可视区域的水平 / 垂直坐标
  • vnode.offsetLeft / vnode.offsetTop属性:当前元素距离某个定位父辈元素左边 / 顶部的距离
  • vnode.offsetWidth / vnode.offsetHeight:当前元素的宽 / 高度,包括边框和填充
  • window.innerHeight / window.innerHeightWidth:获取当前页面可视区的宽高(包括滚动条)
5、组件自定义指令

https://cn.vuejs.org/v2/guide/custom-directive.html

6、组件使用到的相关事件
  • onmousedown 鼠标按下事件
  • onmousemove 鼠标移动事件
  • onmouseup 鼠标松开事件
el.onmousedown = function(e){
    console.log('鼠标已按下:', e)
}
el.onmousemove = function(e){
    console.log('鼠标移动中:', e)
}
el.onmouseup = function(e){
    console.log('鼠标已松开:', e)
}
// 注:el 表示当前触发的元素
7、实现思路
  • 给弹窗绑定onmousedown事件,获取鼠标在弹窗中按下的位置(以弹窗左上角为原点)
el.onmousedown = ((event) => {
  let mouseX = event.clientX - vnode.offsetLeft
  let mouseY = event.clientY - vnode.offsetTop
})
  • document绑定onmousemove事件,获取当前的鼠标位置,当前鼠标位置 - 鼠标在弹窗中的相对位置,通过style设置弹窗的当前位置
document.onmousemove = ((event) => {
  let left, top
  // 获取新的鼠标位置(event.clientX, event.clientY)
  // 弹窗应该在的位置(left, top)
  // (mouseX, mouseY) 鼠标按下时的坐标
  left = event.clientX - mouseX
  top = event.clientY - mouseY
  // 赋值移动
  vnode.style.left = left + 'px'
  vnode.style.top = top + 'px'
})
  • 鼠标松开解绑document的鼠标事件onmousedown,onmousemove
document.onmouseup = (() => {
  document.onmousemove = document.onmouseup = null
})
  • 控制弹窗只能在浏览器可视区内被拖拽,需要设置水平和垂直方向的最大最小移动位置,在“赋值移动”前添加下面代码
// 获取弹窗在页面中距X轴的最小、最大 位置
let minX = -vnode.offsetWidth / 2 + 100
let maxX = window.innerWidth + vnode.offsetWidth / 2 - 100
if (left <= minX) {
  left = minX
} else if (left >= maxX) {
  left = maxX
}
// 获取弹窗在页面中距Y轴的最小、最大 位置
let minY = vnode.offsetHeight / 2
let maxY = window.innerHeight + vnode.offsetHeight / 2 - 100
if (top <= minY) {
  top = minY
} else if (top >= maxY) {
  top = maxY
}
  • 浏览器可视区大小变化时重置弹窗的位置到初始化状态
window.onresize = (() => {
  vnode.style.left = "50%"
  vnode.style.top = "50%"
})
  • 控制鼠标按下弹窗指定区域才能拖拽弹窗,在 onmousedown 事件中添加下面代码
// my_dialog_title 指定区域对应元素的类名
if (event.target.className !== "my_dialog_title") {
  return
}
8、完整拖拽指令

  directives: {
    drag: {
      inserted: function (el, binding, vnode) {
        vnode = vnode.elm
        el.onmousedown = ((event) => {
          if (event.target.className !== "my_dialog_title") {
            return
          }
          // (clientX, clientY)点击位置距离当前可视区域的坐标(x,y)
          // offsetLeft, offsetTop 距离上层或父级的左边距和上边距

          // 获取鼠标在弹窗中的位置
          let mouseX = event.clientX - vnode.offsetLeft
          let mouseY = event.clientY - vnode.offsetTop

          // 绑定移动和停止函数
          document.onmousemove = ((event) => {
            let left, top

            // 获取新的鼠标位置(event.clientX, event.clientY)
            // 弹窗应该在的位置(left, top)
            left = event.clientX - mouseX
            top = event.clientY - mouseY

            // offsetWidth、offsetHeight 当前元素的宽度
            // innerWidth、innerHeight 浏览器可视区的宽度和高度

            // 获取弹窗在页面中距X轴的最小、最大 位置
            let minX = -vnode.offsetWidth / 2 + 100
            let maxX = window.innerWidth + vnode.offsetWidth / 2 - 100
            if (left <= minX) {
              left = minX
            } else if (left >= maxX) {
              left = maxX
            }

            // 获取弹窗在页面中距Y轴的最小、最大 位置
            let minY = vnode.offsetHeight / 2
            let maxY =window.innerHeight + vnode.offsetHeight / 2 - 100
            if (top <= minY) {
              top = minY
            } else if (top >= maxY) {
              top = maxY
            }
            // 赋值移动
            vnode.style.left = left + 'px'
            vnode.style.top = top + 'px'
          })
          document.onmouseup = (() => {
            document.onmousemove = document.onmouseup = null
          })
        })
        window.onresize = (() => {
          vnode.style.left = "50%"
          vnode.style.top = "50%"
        })
      }
    }
  }
9、引用

使用 import 引入
eg: import myDialog from '@/components/myDialog'

<template>
  <div class="my_page">
    <button @click="isShow = true">打开</button>
    <my-dialog class="dialog" :visible.sync="isShow" :title="title" :canceltext="canceltext" :confirmtext="confirmtext" @confirm="onConfirm" @cancel="onCancel">
      这是个弹窗
    </my-dialog>
  </div>
</template>
<script>
// 引入弹窗
import myDialog from '@/components/myDialog'
export default {
  data() {
    return {
      isShow: false,
      title: '我的弹窗',
      canceltext: '关闭',
      confirmtext: '提交'
    }
  },
  components: {
    myDialog
  },
  methods: {
    onConfirm() { },
    onCancel() { }
  }
}
</script>
<style>
.my_page {
  text-align: center;
}
</style>
10、CSS
<style>
.my_dialog {
  position: fixed;
  z-index: 99;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
}
.my_dialog_mask {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
  background-color: #000;
  opacity: 0.5;
}
.my_dialog_box {
  position: absolute;
  width: 550px;
  background: #fff;
  top: 50%;
  left: 50%;
  max-width: 100%;
  border-radius: 3px;
  overflow: hidden;
  transform: translate(-50%, -50%);
}
.my_dialog_content {
  min-height: 100px;
  overflow-x: hidden;
  overflow-y: auto;
  position: relative;
  padding: 20px;
  text-align: left;
  box-sizing: border-box;
}
.my_dialog_title {
  cursor: all-scroll;
  word-break: keep-all;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  position: relative;
  top: 0;
  left: 0;
  height: 40px;
  line-height: 40px;
  border-bottom: 1px solid #e7e8eb;
  color: #000;
  font-size: 18px;
  font-family: \5fae\8f6f\96c5\9ed1;
  padding: 0 31px 0 18px;
  text-align: left;
  user-select: none;
}
.my_dialog_close {
  cursor: pointer;
  position: absolute;
  top: 50%;
  margin-top: -8px;
  right: 20px;
  width: 16px;
  height: 16px;
  line-height: 16px;
  color: #ccc;
}
.my_dialog_close:hover {
  color: #409eff;
}
.my_dialog_bottom {
  margin: 0;
  padding: 16px 0;
  text-align: center;
  border-top: 1px solid transparent;
}
.btn {
  min-width: 60px;
  text-align: center;
  vertical-align: middle;
  font-size: 14px;
  padding: 5px 15px;
  border-radius: 3px;
  text-decoration: none;
  border-radius: 3px;
  cursor: pointer;
}
.my_dialog_bottom .cancelBtn:focus,
.my_dialog_bottom .cancelBtn:hover {
  color: #409eff;
  background: #ecf5ff;
  border: 1px solid #b3d8ff;
}
.my_dialog_bottom .confirmBtn:focus,
.my_dialog_bottom .confirmBtn:hover {
  background: #66b1ff;
  border: 1px solid #66b1ff;
  color: #fff;
}
.my_dialog_bottom .confirm_btn .marginLeft {
  margin-left: 10px;
}
.cancelBtn {
  border: 1px solid #dcdfe6;
  background-color: #fff;
  color: #606266;
}
.confirmBtn {
  border: 1px solid #409eff;
  background-color: #409eff;
  color: #fff;
}
button + button {
  margin-left: 15px;
}
</style>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342