用vue写轮子的一些心得(四)——Popover组件

需求分析

Poppver组件通常包括一下功能:

  • 点击按钮根据按钮位置弹出弹出层,弹出层始终保持在最上方
  • 可以定义弹出位置,支持上下左右弹出
  • 支持可点击弹出内容,弹出层不会消失
  • 弹出内容支持插入纯文字或者html结构
  • 支持鼠标移入弹出,移出消失
  • 支持鼠标点击按住不动弹出,松开消失

 

方法实现

1、定义组件:

在html中定义

弹出层内容与按钮样式统一规定写在popover组件标签中,在popover组件中将会用到。

    <t-popover>
        <template slot="content">
            <div>鼠标点击弹出,再次点击别处消失</div>
        </template>
        <t-button>钮</t-button>
    </t-popover>

popover组件html定义

通过两个slot接收父组件传来的参数。其中一个slot命名为content,接收父组件名字为content的参数,这样无论是纯文本还是html结构都可以通过slot展示出来。

因为Vue中的slot不能直接写样式以及ref,因此我们在slot外面包裹一层元素标签用来写样式和标注ref,以便接下来在js中通过ref控制dom,将弹出层内容dom固定到按钮附近指定的位置。

<template>
    <div ref="popover" class="popover">
        <div v-if="visible" ref="contentWrapper" class="content-wrapper" :class="{[`position-${position}`]: true}">
            <slot name="content" :close="close"></slot>
        </div>
        <!-- span标签增加display: inline-block; 解决包裹元素高度一致的问题 -->
        <span ref="triggerWrapper" style="display: inline-block;">
            <slot></slot>
        </span>
    </div>
</template>

2、js相关的逻辑:

当点击按钮时,弹出层通过 

document.body.appendChild(this.$refs.contentWrapper)

 dom操作将内容元素动态加入到html最后面,然后通过

const {contentWrapper,triggerWrapper} = this.$refs
const {width, height, top, left} = triggerWrapper.getBoundingClientRect()

找到相对页面popover组件位置,根据top、left的位置将弹出层绝对定位到按钮附近指定的位置处即可,上下左右位置通过top、left的值计算得出即可。

但是这样点击按钮弹出层显示的逻辑是完成了。隐藏该如何做呢?

一开始可以试着点击页面任何位置,执行隐藏弹出层操作,但是这样会有BUG,点击弹出层时弹出层也会被隐藏。因此需要执行如下代码

            onClickDocument(e) {
                if (this.$refs.contentWrapper && this.$refs.contentWrapper.contains(e.target)) {return}
                this.close()
            },

当点击在popover 或者内容弹出层时,直接return出去。只有点击这两个地方以外才会隐藏内容弹出层。

 

鼠标移入显示弹出层,离开隐藏弹出层,和鼠标点击按住不动弹出,松开消失相关功能:

通过props传入的参数,判断为click,hover还是focus。然后在生命周期mounted中执行相应的事件监听。

html

    <t-popover position="right" trigger="hover/focus">
        <template slot="content">
            <div>鼠标移入弹出,移出消失</div>// hover
            <div>鼠标点击按住不动弹出,松开消失</div>// focus
        </template>
        <t-button>钮</t-button>
    </t-popover>

js

        props: {
            trigger: {
                type: String,
                default: 'click',
                validator(value) {
                    return ['click', 'hover', 'focus'].indexOf(value) >= 0
                }
            },
        },
        mounted() {
            const popover = this.$refs.popover
            if (this.trigger === 'click') {
                popover.addEventListener('click', this.onClick)
            } else if (this.trigger === 'hover') {
                popover.addEventListener('mouseenter', this.open)// 添加hover监听事件
                popover.addEventListener('mouseleave', this.close)// 取消hover监听事件
            } else {
                popover.addEventListener('mousedown', this.open)// 添加hover监听事件
                popover.addEventListener('mouseup', this.close)// 取消hover监听事件
            }
        },

另外,需要记住当当前组件销毁时,需要取消所有的监听事件,节省性能。

        destroyed() { // 页面销毁的时候去掉监听
            const popover = this.$refs.popover
            if (this.trigger === 'click') {
                popover.removeEventListener('click', this.open())
            } else if (this.trigger === 'hover') {
                popover.removeEventListener('mouseenter', this.open)// 添加hover监听事件
                popover.removeEventListener('mouseleave', this.close)// 取消hover监听事件
            } else {
                popover.removeEventListener('mousedown', this.open())
                popover.removeEventListener('mouseup', this.close())
            }
        },

完整代码: 

    export default {
        name: "tPopover",
        components: {},

        props: {
            position: {
                type: String,
                default: 'top',
                validator(value) {
                    return ['top', 'bottom', 'left', 'right'].indexOf(value) >= 0
                }
            },
            trigger: {
                type: String,
                default: 'click',
                validator(value) {
                    return ['click', 'hover', 'focus'].indexOf(value) >= 0
                }
            },
        },

        data() {
            return {
                visible: false,
            }
        },

        computed: {},

        mounted() {
            const popover = this.$refs.popover
            if (this.trigger === 'click') {
                popover.addEventListener('click', this.onClick)
            } else if (this.trigger === 'hover') {
                popover.addEventListener('mouseenter', this.open)// 添加hover监听事件
                popover.addEventListener('mouseleave', this.close)// 取消hover监听事件
            } else {
                popover.addEventListener('mousedown', this.open)// 添加hover监听事件
                popover.addEventListener('mouseup', this.close)// 取消hover监听事件
            }
        },

        destroyed() { // 页面销毁的时候去掉监听
            const popover = this.$refs.popover
            if (this.trigger === 'click') {
                popover.removeEventListener('click', this.open())
            } else if (this.trigger === 'hover') {
                popover.removeEventListener('mouseenter', this.open)// 添加hover监听事件
                popover.removeEventListener('mouseleave', this.close)// 取消hover监听事件
            } else {
                popover.removeEventListener('mousedown', this.open())
                popover.removeEventListener('mouseup', this.close())
            }
        },

        methods: {
            positionContent() {
                document.body.appendChild(this.$refs.contentWrapper)
                const {contentWrapper,triggerWrapper} = this.$refs
                const {width, height, top, left} = triggerWrapper.getBoundingClientRect()
                let positions = {
                    top: {
                        top: top + window.scrollY,
                        left: left + window.scrollX,
                    },
                    bottom: {
                        top: top + height + window.scrollY,
                        left: left + window.scrollX,
                    },
                    left: {
                        top: top + window.scrollY,
                        left: left + window.scrollX,
                    },
                    right: {
                        top: top + window.scrollY,
                        left: left+ width + window.scrollX,
                    },
                }
                contentWrapper.style.left = positions[this.position].left + 'px'
                contentWrapper.style.top = positions[this.position].top + 'px'
            },
            onClickDocument(e) { // 如果点击在popover 则让popover自己去处理,document不管
                if (this.$refs.contentWrapper && this.$refs.contentWrapper.contains(e.target)) {return}
                this.close()
            },
            open() {
                this.visible = true
                 setTimeout(() => {
                    this.positionContent()
                     document.addEventListener('click', this.onClickDocument)
                 })
            },
            close() {
                this.visible = false
                document.removeEventListener('click', this.onClickDocument)
            },
            onClick(event) {
                if (this.$refs.triggerWrapper.contains(event.target)) { // 找到点击事件的元素
                    if (this.visible) {
                        this.close()
                    } else {
                        this.open()
                    }
                }
            },
        }
    }

 

表驱动优化法

上面有一处代码用if/else 的写法为:

            positionContent() {
                document.body.appendChild(this.$refs.contentWrapper)
                const {contentWrapper,triggerWrapper} = this.$refs
                let {width, height, top, left} = triggerWrapper.getBoundingClientRect()
                if (this.position === 'top') {
                    contentWrapper.style.left = left + window.scrollX + 'px'
                    this.$refs.contentWrapper.style.top = top + window.scrollY + 'px'
                } else if (this.position === 'bottom') {
                    contentWrapper.style.left = left + window.scrollX + 'px'
                    contentWrapper.style.top = top + height + window.scrollY + 'px'
                } else if (this.position === 'left') {
                    contentWrapper.style.left = left + window.scrollX + 'px'
                    contentWrapper.style.top = top + window.scrollY + 'px'
                } else if (this.position === 'right') {
                    contentWrapper.style.left = left+ width + window.scrollX + 'px'
                    contentWrapper.style.top = top + window.scrollY + 'px'
                }
            },

当position为top时如何如何,当potision为bottom时如何如何··· 这是我们常用的if/else的判断流程写法,但判断太多看起来就很累了。怎么办?

通过表驱动的思想进行优化,什么是表驱动?

通过一张表,将条件中的交集罗列出来,这样我们就好优化了。

优化后如下:看起来更直接好懂,省略了if/else ,用对象模拟一张表将全部判断条件写进对象中,然后通过positions[this.position].left + 'px' 对象的链式写法即可完成 if/else 相同的功能。

            positionContent() {
                document.body.appendChild(this.$refs.contentWrapper)
                const {contentWrapper,triggerWrapper} = this.$refs
                const {width, height, top, left} = triggerWrapper.getBoundingClientRect()
                let positions = {
                    top: {
                        top: top + window.scrollY,
                        left: left + window.scrollX,
                    },
                    bottom: {
                        top: top + height + window.scrollY,
                        left: left + window.scrollX,
                    },
                    left: {
                        top: top + window.scrollY,
                        left: left + window.scrollX,
                    },
                    right: {
                        top: top + window.scrollY,
                        left: left+ width + window.scrollX,
                    },
                }
                contentWrapper.style.left = positions[this.position].left + 'px'
                contentWrapper.style.top = positions[this.position].top + 'px'
            },

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值