想要让元素可以在浏览器的视窗中移动起来,首先最重要的一步就是,圈定可移动的范围
css 样式的核心代码
// 父容器核心样式
position: fixed;
width: 100%;
height: 100%;
// 子容器核心样式
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
父容器通过 width && height 字段占满整个浏览器的可视范围,子容器通过 position: absolute 属性开启在父容器内的绝对定位,在通过 top && left && transform: translate(-50%, -50%) 属性控制子容器在父容器内的绝对居中位置
JavaScript 逻辑控制的核心代码
首先分解下,要实现 node 的移动需要哪些步骤和对应的 event 事件
- 子容器创建时,在父容器内的绝对位置
- 鼠标按键按下时,onmousedown 事件
- 鼠标移动时,onmousemove 事件
- 鼠标按键弹起时,onmouseup 事件
只要使用 onMousedown、onMousemove和onMouseup 这三个事件,就可以实现最简单的移动
/*
* 在子容器创建的时候获取子容器相对于父容器的 top 和 left 位置
*/
mounted () {
this.left = this.$refs.fatherBox.offsetLeft
this.top = this.$refs.fatherBox.offsetTop
}
/*
* 鼠标按下时
* 1. 开启允许子容器移动的 flag
* 2. 记录鼠标点击时的位置信息
*/
mouseDown (e) {
// 设置允许弹窗移动的 flag
this.moveFlag = true
// 保存鼠标开始位置
this.startLeft = e.clientX - this.left
this.startTop = e.clientY - this.top
}
/*
* 鼠标移动时
* 1. 判断 flag 是否允许子容器移动
* 2. 设置弹框左边位置
* 3. 设置弹框右边位置
*/
move (e) {
// 判断 flag 是否允许移动
if (!this.moveFlag) return
// 设置弹框左边位置
this.left = e.clientX - this.startLeft
// 设置弹框右边位置
this.top = e.clientY - this.startTop
}
/*
* 鼠标按键弹起时
* 1. 关闭允许子容器移动的 flag
*/
mouseUp (e) {
this.flag = false
}
通过这几个方法就可以获取鼠标按下移动时,鼠标的top 和 left 的偏移量,通过把这偏移量暴露出去给父组件,父组件实时设置子组件的 top 和 left 值,来使得子容器跟随鼠标的移动
父组件代码
父组件通过设置子组件的 ref、zIndex 值,而且父组件的 backValue 方法会从子组件接收 zIndex 值,通过 zIndex 来识别具体的子组件实例
父组件需要的 childList 属性也是被传递进来的,所以 close 方法必须要把子组件的 index 暴露出去进行 childList 的操作,当然你也可以使用 $parent 直接操作 childList 的数组,但是我强烈不建议这样操作
这里我采用的是 $EventBus 注册了一个事件,如果知道确定的数据流,和嵌套的结构,也可以直接在 props 里传递一个函数,然后用 close 方法调用传递进来的函数,把数据暴露出去
/*
* 父组件代码片段 jsx 版
*/
/**
* @param childList required: true, type: Array, 子组件需要的数据列表
* @param maxChild required: false, type: Number, default: 4, 允许最多多少个子组件同时存在
*/
export default {
props: {
childList: {
type: Array,
required: true
},
maxChild: {
type: Number,
default: 4
}
},
data () {
return {
}
},
render () {
return (
<div class={'mvp-father-box'} ref={'father'}>
{
this.playList && this.childList.map((item, index) => {
if (this.maxChild - 1 < index) return null
return (
<ChildComponents
key={index}
ref={index}
zIndex={index}
visible={item !== null}
backValue={this.backValue}
info={item !== null ? item : {}}
close={this.close}
width={600}
height={400}
/>
)
})
}
</div>
)
},
methods: {
backValue (left, top, zIndex) {
this.$refs[zIndex].$el.style.top = `${top}px`
this.$refs[zIndex].$el.style.left = `${left}px`
},
close (index) {
this.$EventBus.$emit('deletePlayerIndex', index)
}
}
}
设置子组件的围栏范围
当然,如果不设定围栏范围,子组件是会移出浏览器的可视范围的哦
设置移动围栏只要在 onmousemove 事件中进行判断 子组件的 top 和 left 是否超出设定的可视范围就可以了
完整的子组件代码如下
/*
* 子组件代码片段 jsx 版
*/
/**
* @param visible required: true, type: Boolean 控制组件是否显示
* @param info required: true, type: Object 接受的数据信息
* @param close required: false, type: Function 关闭组件的回调
* @param backValue required: true, type: Function(left: Number, top: Number, zIndex: Number) 回调函数,把弹框的移动距离传递给父组件
* @param width required: false, type: String || Number, default value: 350px 子组件的宽度
* @param height required: false, type: String || Number, default value: 270px 子组件的高度
* @param zIndex required: false, type: Number, default value: 1 子组件的索引
*/
export default {
props: {
visible: {
type: Boolean,
default: false,
required: true
},
info: {
type: Object,
required: true,
default: () => {}
},
close: {
type: Function,
default: () => {}
},
backValue: {
type: Function,
required: true
},
width: {
type: Number,
default: 350
},
height: {
type: Number,
default: 270
},
zIndex: {
type: Number,
default: 1
}
},
data () {
return {
moveFlag: false,
startLeft: 0,
startTop: 0,
left: 0,
top: 0,
constLeft: 0,
constTop: 0
}
},
render () {
if (!this.visible) return null
return (
<div
class={'vpm-father-box'}
onMousedown={(event) => this.mouseDown(event)}
onMousemove={(event) => this.move(event)}
onMouseout={(event) => this.mouseOut(event)}
onMouseup={(event) => this.mouseUp(event)}
ref={'fatherBox'}
style={{ width: `${this.width}px`, height: `${this.height}px`, zIndex: 2000 }}
>
<div class={'vpm-title-box'}>
<div class={'vpm-title'}>
{this.info?.positionName ? this.info.positionName : '测试用例'}
</div>
<div class={'vpm-close-box'} onClick={(event) => this.closeModal(event)} />
</div>
<div>
</div>
</div>
)
},
mounted () {
this.constLeft = this.left = this.$refs.fatherBox.offsetLeft
this.constTop = this.top = this.$refs.fatherBox.offsetTop
},
methods: {
closeModal (e) {
// 禁止事件冒泡
e.stopPropagation()
// 关闭的时候,把弹框的数据重置
this.startLeft = 0
this.startTop = 0
this.left = this.constLeft
this.top = this.constTop
if (this.close) {
this.close(this.zIndex)
}
},
// 鼠标按下时
mouseDown (e) {
// 设置允许弹窗移动的 flag
this.moveFlag = true
// 保存鼠标开始位置
this.startLeft = e.clientX - this.left
this.startTop = e.clientY - this.top
},
// 鼠标在组件中移动时
move (e) {
// 判断 flag 是否允许移动
if (!this.moveFlag) return
// 判断回调函数是否存在
if (!this.backValue) return
// 判断是否超出左边视图
if (this.$refs.fatherBox.offsetLeft < this.width / 2) {
// 禁止弹框移动
this.moveFlag = false
// 设置弹框左边位置
this.left = this.width / 2 + 10
// 调用回调函数
this.backValue(this.left, this.top, this.zIndex)
return
}
// 判断是否超出右边视图
if (this.$refs.fatherBox.offsetLeft > document.body.clientWidth - this.width / 2) {
// 禁止弹框移动
this.moveFlag = false
// 设置弹框右边位置
this.left = document.body.clientWidth - this.width / 2 - 10
// 调用回调函数
this.backValue(this.left, this.top, this.zIndex)
return
}
// 判断是否超出顶部视图
if (this.$refs.fatherBox.offsetTop < this.height / 2 + 70) {
// 禁止弹框移动
this.moveFlag = false
// 设置弹框顶部位置
this.top = this.height / 2 + 70 + 10
// 调用回调函数
this.backValue(this.left, this.top, this.zIndex)
return
}
// 判断是否超出底部视图
if (this.$refs.fatherBox.offsetTop > document.body.clientHeight - this.height / 2 - 50) {
// 禁止弹框移动
this.moveFlag = false
// 设置弹框底部位置
this.top = document.body.clientHeight - this.height / 2 - 50 - 10
// 调用回调函数
this.backValue(this.left, this.top, this.zIndex)
return
}
// 设置弹框左边位置
this.left = e.clientX - this.startLeft
// 设置弹框右边位置
this.top = e.clientY - this.startTop
// 调用回调函数
this.backValue(this.left, this.top, this.zIndex)
},
// 鼠标按键弹起时
mouseOut (e) {
// 禁止弹框移动
this.moveFlag = false
},
// 鼠标移出组件时
mouseUp (e) {
// 禁止弹框移动
this.moveFlag = false
}
}
}
操作传递进父组件的 childList
childList 是调用父组件时传递进来的一个数组,父组件通过这个数组来进行渲染子组件的个数,所以对这个数组的操作不正确,就会引发比较神奇的 bug
// 使用注册事件删除 数组中不需要的项
mounted () {
this.$EventBus.$on('deletePlayerIndex', (index) => {
this.playList.splice(index, 1, null)
})
},
使用 null 来填充删除的项,而不是直接切片,直接切片会导致原数组的长度变化,而经过 vue 的渲染的时候,会导致 node 重新渲染,而且关闭的子元素并不是我们所期望的那一个
因为是用 null 来填充,所以添加新元素的时候,同样也要进行特殊处理
一共有三种情况:
- 当 childList 的长度为 0 时,直接使用
push方法把项加入到最后 - 当 childList 的长度不为 0 且长度小于 4,同样直接使用
push方法把项加入到最后 - 当 childList 的长度不为 0,循环数组,找到第一个为
null的项,然后用新数据替换
// 添加项进 childList
switch (true) {
case this.childList.length === 0:
this.childList.push(item)
break
case this.childList.length !== 0:
if (this.childList.length < 4) {
this.childList.push(item)
break
}
for (let i = 0; i < 3; i++) {
if (this.childList[i] === null) {
this.childList.splice(i, 1, item)
break
}
}
break
}