阅读 240

popup位置自适应组件的实现思路与实践

需求分析:

  • 组件可设置弹出位置(placement),支持top、bottom

  • 气泡弹窗弹出位置计算,边界计算,支持设置边界范围

  • 支持点击弹窗元素之外的区域,弹窗关闭

  • 支持弹窗内容自定义

  • 效果预览:

3.gif

实现方案

实现自定义组件, 我们一般都会想到Vue.extendvue.extend相当于一个扩展实例构造器,用于创建一个具有初始化选项的Vue子类,在实例化时可以进行扩展选项,最后使用$mount方法绑定在元素上。

先写一个简单的demo

  • 编写index.vue文件

<template>   <div     v-if="visible">     <div       ref="arrowDom"       :style="{left: `${arrowLeft}px`}"></div>     <div>       <div>{{ txt }} </div>     </div>   </div> </template> <script> export default {   name: 'Popover',   props: {     txt: {       type: String,       default: ''     }   },   data: () => ({     arrowLeft: 51,     visible: false,     positionStyle: { left: '15px', top: '69.5781px' }   }),   methods: {     open() {       this.visible = !this.visible;     }   } }; </script> <style> .popover {   position: absolute; } </style> 复制代码

注意:目前组件接收一个txt参数,且弹窗的位置信息有css固定,js只是提供了显示逻辑。气泡弹窗针对body元素绝对定位

  • 编写index.js。 点击按钮时,动态追加弹窗元素到body上

// index.js import Vue from 'vue'; // 导入刚才我们写的index.vue import DialogCompt from './index.vue'; let component; // 保证只存在一个组件实例 const createComponents = function() {   if (!component) {     const DialogConstructor = Vue.extend(DialogCompt);     component = new DialogConstructor({       el: document.createElement('div')     });   }   return component; }; const preview = (options) => {   component = createComponents();   document.body.appendChild(component.$el);   component.txt = options.txt;   component.open(); }; export default preview; 复制代码

  • 项目中使用

import preview from './index'; methods: {      // 组件调用       peview({         txt: '我是文字文字文字文字文字文字文字文字'       }); } 复制代码

经过上面三个步骤,一个简单的弹窗就出来啦,效果预览:

post-img2

处理相关的点击交互

  • 点击弹窗本身,弹窗不消失

可以判断当前点击的对象是否在弹窗范围内,通过Node.contains这个方法

  • 点击弹窗之外的元素,弹窗消失

一般做法就是在弹窗显示之后,给document添加一个click事件,现在我们来修改上面的index.vue

<script> methods: {   documentEventHandler() {     // 如果点击弹窗自身时,不触发隐藏逻辑     if (!this.$el.contains(evt.target)) {       this.close();     }   },   close() {     this.visible = false;    // 弹窗关闭后记得移除click事件     document.removeEventListener('click', this.documentEventHandler);   },   open() {       // ...       this.$nextTick(() => {         // 弹窗显示后,给document对象注册click事件         document.addEventListener('click', this.documentEventHandler);       });       // ...   } } </script> 复制代码

效果预览:

4.gif

position计算

  • 获取目标元素相关信息

这个需要在调用组件对象的时候,把点击对象传组件内部

showPop(event) {   // 组件调用,把event传到组件内部,方便组件内部获取目标元素各种尺寸信息   review({     event,     txt1: '我是文字文字文字文字文字文字文字文字'   }); } 复制代码

  • 获取点击对象的相关尺寸信息

通过传入的event对象,我们可以拿到当前的currentTarget: event.currentTarget; 然后使用Element.getBoundingClientRect()这个方法获取currentTarget的left、right、top、bottom信息

// index.js const preview = (options) => {   component = createComponents();   const event = options.event;   event.stopPropagation();   const currentTarget = event.currentTarget;   const { left, right, bottom, top } = currentTarget.getBoundingClientRect();   component.targetPosition = { left, right, bottom, top };   component.placement = options.placement;   // other code }; 复制代码

  • 计算弹窗的left、top值

post-img8

根据上图标注,获取弹窗左上角的left、top值,思路如下:

  • 1.获取目标元素距离四边距屏幕左、右、上、下侧的边距

  • 2.按照弹窗水平对齐目标元素的思路,计算出left值,这里需要考虑左右边界问题

  • 3.根据弹窗自身的高度、箭头高度、间隙高度及目标元素上边距距离屏幕顶部、目标元素下边距距离屏幕顶部的高度来计算弹窗的top值,这里也需要考虑上下边界问题

根据上面的思路,我们可以这么干: left = 目标元素距离屏幕右侧的距离 + 目标元素宽度 / 2 - 弹窗自身宽度 / 2

弹窗top值也是同样的道理: 显示在元素上方: top = 目标元素上边距距离屏幕顶部的距离 - 弹窗自身宽度 - 箭头高度 - 偏移量

显示在元素下方: top = 目标元素下边距距离屏幕顶部的距离 + 箭头高度 + 偏移量 下面我们来修改下index.vue,

props: {   targetPosition: {     type: Object,     default: () => ({})   },   // 弹窗显示的位置, 默认现在是触发元素底部   placement: {     type: String,     default: 'bottom'   },   // 弹窗距离屏幕左侧的最小距离   minLeft: {     type: Number,     default: 30   },   // 弹窗距离屏幕右侧的最小距离   minRight: {     type: Number,     default: 30   },   // 弹窗距离触发元素的间距, 默认8px   offset: {     type: Number,     default: 8   } }, data: () => ({   arrowLeft: 0,   positionStyle: { left: '-100%', top: '0' } }), method: {   open() {     // ...     this.$nextTick(() => {       // 开始计算弹窗显示的位置       this.calcPosition();     });     // ...   },   calcPosition() {     // 拿到弹窗的宽、高信息     const { width, height } = this.$el.getBoundingClientRect();     const targetWidth = this.targetPosition.right - this.targetPosition.left;     // 计算弹窗距屏幕左边距的位移     let popLeft = this.targetPosition.left + targetWidth / 2 - width / 2;     // 计算弹窗距屏幕右边距的最小位移     const maxLeft = document.body.clientWidth - width - this.minRight * window.rem / 72;     if (popLeft < this.minLeft * window.rem / 72) {       popLeft = this.minLeft * window.rem / 72;     } else if (popLeft > maxLeft) {       popLeft = maxLeft;     }     const arrowWidth = this.$refs.arrowDom.getBoundingClientRect().width;     const arrowHeight = this.$refs.arrowDom.getBoundingClientRect().height;     let popTop;     if (this.placement === 'bottom') {       // 显示在按钮底部       popTop = this.targetPosition.bottom + this.offset * window.rem / 72 + arrowHeight;     } else {       // 显示在按钮顶部       popTop = this.targetPosition.top - height - arrowHeight - this.offset * window.rem / 72;     }     // 箭头是针对弹窗定位的, 所以记得以弹窗的左上角或者右下角来计算位移     if (popLeft === this.minLeft * window.rem / 72) {       this.arrowLeft = this.targetPosition.left - this.minLeft * window.rem / 72 + targetWidth / 2 - arrowWidth / 2;     } else if (popLeft === maxLeft) {       this.arrowLeft = this.targetPosition.left + targetWidth / 2 - popLeft - arrowWidth / 2;     } else {       this.arrowLeft = (popLeft + width - popLeft) / 2 - arrowWidth / 2;     }     this.positionStyle = {       left: `${popLeft}px`,       top: `${popTop}px`     };   } } 复制代码

测试的时候发现,如果页面内容超出了一屏,页面发生滚动之后,点击按钮,弹窗的定位会出现偏移。这是为什么呢??? 噢!!!原来我们之前只是计算了点击元素距离视口顶部的距离,但是弹窗是针对整个body定位的, 所以弹窗的真实top值还需要把页面滚动的距离算上。 document.documentElement.scrollTop就是我们要计算的,记得做下兼容,可参考。 那么此时元素的距离body顶部的距离应该是: 元素距离视口顶部的高度 + 容器滚动的高度

// index.js const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; this.positionStyle = {   left: `${popLeft}px`,   top: `${popTop + scrollTop}px` }; 复制代码

现在滚动页面,点击按钮,预览弹窗位置显示正常啦。

5.gif

编写支持自定义内容弹窗组件

上面的实现方式,只能实现固定模板内容,如果后期遇到其他气泡弹窗,但是内容不同的时候,这个组件就得修改才能用,所以还有一种实现方式:采用具名插槽来实现popover框架,template、css部分可由调用者自由定义。 那么理想情况下,用户可以这么调用(假如我们的组件是:im-popover):

<im-popover v-model="visible">   <div><!--弹窗内容--></div>   <button slot="trigger">触发元素</button> </im-popover> 复制代码

弹窗内容是默认插槽,按钮元素需要声明slot="trigger"

  • 首先实现一个最基本的,我们点击trigger元素显示这个弹窗内容

<!--ImPopover.vue--> <template>   <div @click="handleClick">     <!--弹窗主体部分-->     <div       v-show="visible"       ref="popoverDom"       :style="positionStyle">       <div         ref="arrowDom"         :style="{left: `${arrowLeft}px`}"       ></div>       <!--可自定义内容,默认插槽-->       <slot></slot>     </div>     <!--触发弹窗元素-->     <slot name="trigger"></slot>   </div> </template> <style> .im-popover {   z-index: 1000;   position: absolute;   max-width: 660px;   &__arrow {     // ...   }   &__box {     /* 这里需要设置成行内元素 */     display: inline-block;   } } </style> 复制代码

  • 编写对应的script脚本

其实就是之前的index.js和index.vue的结合,不过需要把this.$el改成this.$refs.popoverDom

<script> export default {   name: 'ImPopover',   props: {     // ...   },   methods: {     open() {       if (!this.visible) {         // 把popover组件追加到body尾部         this.appendContainer();         // ...         })       } else {         this.close();       }     },     appendContainer() {       document.body.appendChild(this.$refs.popoverDom);     },     handleClick(event) {       // 阻止冒泡       event.stopPropagation();       this.open();     },     // ...   } } </script> 复制代码

  • im-popover组件调用

<template> <im-popover placement="top">   <div>     <img src="https://static-web.likeevideo.com/as/indigo-static/test/diamond.png" alt="">     <p>原始文字是人类用来紀錄特定事物、簡化圖像而成的書寫符號。 文字在发展早期都是图画形式的,有些是以形表意</p>   </div>   <div slot="trigger" class="app__button app__button--small">自定义弹窗内容</div> </im-popover> </template> <script> import ImPopover from './ImPopOver'; export default {   name: 'App',   components: {     ImPopover   } } </script> <style> .popover-content {   width: 468px;   padding: 12px;   box-sizing: border-box;   display: flex;   align-items: center;   flex-direction: column;   .image {     width: 148px;     height: auto;     vertical-align: top;   }   .txt {     margin-top: 20px;     font-size: 28px;     color: #fff;     text-align: center;   } } </style> 复制代码

  • 效果预览

6.gif

总结

好了,组件的两种实现方法都讲完了,其实popover组件涉及的功能比较多,比如有弹窗触发方式的配置(例如click、hover)、弹窗内容是否支持滚动和点击、弹窗的显示与隐藏回调、弹窗动画等等功能。这个等到以后组件需要拓展时再考虑吧。 如果你看到这篇文章,希望对你了解popover组件开发原理有所帮助。

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。


作者:bigo前端
链接:https://juejin.cn/post/7033985356525469709

文章分类
前端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐