Popover组件不一样于alert这种霸道总裁, 它更倾向于辅助显示某些未显示完整的内容, toast组件与其相比更偏向'提示', Popover更偏向于'展现', 但属于一种'轻展现', 毕竟不会出现'蒙层'等效果.
别看它小小的, 它里面的门道还很多, 最主要的就是他的定位问题, 好比说它设定为出如今元素上方, 但是元素本身已经在最顶上了, 此时就须要给他'换个方位'展现了, 关于这个定位的计算方式还能够在其余组件上应用, 好比下一集要写的'日期组件', 还有就是这个弹出框的消失时机, 本人更推荐只要滚动就清除它, 每次计算他的位置所消耗的性能很高的, 由于每次都会触发重排与重绘, 话很少说本次咱们就一块儿来搞一搞这个小东西.🏀 css
效果展现vue
vue-cc-ui/src/components/Popover/index.jsgit
export { default } from './main/index';
vue-cc-ui/src/components/Popover/main/popover.vuegithub
<template> // 老套路, 父级 <div class="cc-popover" ref='popover'> // 内容区域 <div class="cc-popover__content" ref='content'> // 这里分了两层是为了解决一会遇到的问题的 <div class="cc-popover__box"> <slot name="content"> 请输入内容</slot> </div> </div> // 这个是被包裹的元素; // 要用咱们的popover标签起来才有效; <slot /> </div> </template>
export default { name: "ccPopover", props: { // 事件类型用户本身传, 本次只支持两种模式 trigger: { type: String, default: "hover", // 这里为了扩展因此这样写 // 只有两种状况能够优化为只要不是click就默认给hover validator: value => ["click", "hover"].indexOf(value) > -1 }, placement: { // 方位咱们定位的范围是, 每一个方向都有'开始','中间','结束'三种状况 type: String, default: "right-middle", validator(value) { let dator = /^(top|bottom|left|right)(-start|-end|-middle)?$/g.test( value ); return dator; } } },
初始化项目的一些操做
经过用户的输入, 来给dom添加监听事件
下面的on 方法 实际上是借鉴了element-ui的写法, 有所收获.element-ui
mounted() { this.$nextTick(() => { // 获取到当前用户定义的事件类型 let trigger = this.trigger, // 本次选择操做dom popover = this.$refs.popover; if (trigger === "hover") { // hover固然要监听 进入与离开的事件拉 on(popover, "mouseenter", this.handleMouseEnter); on(popover, "mouseleave", this.handleMouseLeave); } else if (trigger === "click") { on(popover, "click", this.handlClick); } }); },
on方法的封装
element还判断了是否是服务器环境等操做, 咱们这里只选取了浏览器端相关的代码.数组
vue-cc-ui/src/assets/js/utils.js浏览器
// 添加事件, element-ui判断是否是服务器环境 export function on(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } } // 移除事件 export function off(element, event, handler) { if (element && event) { element.removeEventListener(event, handler, false); } }
假设用户传入的事件类型是'click', mounted里面的操做已经绑定了相应的事件'handlClick',接下来的任务是:性能优化
思路概述服务器
handlClick() { // 无论怎么样只要触发一次, 这个值就会把v-if永远置成true; this.init = true; // 在他自己被css属性隐藏的时候 if (this.$refs.content && this.$refs.content.style.display === "none") { // 必须这样强制写, // 不然与以后的代码配合时, 有bug没法消失 this.$refs.content.style.display = "block"; this.show = true; } else { // 除了第一次以外, 以后都只是变换这个this.show的'真假' this.show = !this.show; } // 不要监听body, 由于可能height不是100%; // 这个document其实也能够由用户指定 // 放入让popover消失的函数, 这样方便以后的移除事件操做 this.show && document.addEventListener("click", this.close); },
点击消失事件app
close(e) { // 确定要判断事件源究竟是不是我们的popover组件 if (this.isPopover(e)) { this.show = false; // 点击完就能够移除了, 下次操做再绑定就能够 // 由于若是往document绑定太多事件, 会很是卡, 很是卡 document.removeEventListener("click", this.close); } },
isPopover
isPopover(e) { let dom = e.target, popover = this.$refs.popover, content = this.$refs.content; // 1: 点击popover包裹的元素, 关闭popover // 2: 点击popover内容区元素, 不关闭popover return !(popover.contains(dom) || content.contains(dom)); },
上面讲述了具体的出现与消失的逻辑, 接下来咱们来让他真正的出如今屏幕上
watch: { // 咱们会监控v-if的状况, 第一次渲染的时候才作这里的操做, 并且只执行一次 init() { this.$nextTick(() => { let trigger = this.trigger, dom = this.$refs.content, content = this.$refs.content; // 这里有人会有疑问, 这什么鬼写法 // 这里是由于append操做属于剪切, 因此不会出现两个元素 // 其实这个元素出现以后就一直存在与页面上了, 除非销毁本组件 // 组件销毁的时候, 咱们会document.body.removeChild(content); document.body.appendChild(dom); if (trigger === "hover") { on(content, "mouseenter", this.handleMouseEnter); on(content, "mouseleave", this.handleMouseLeave); } }); }, // 这个才是每次显示隐藏都会触发的方法 show() { // 判断只有显示提示框的时候才回去计算位置 if (this.show) { this.$nextTick(() => { let { popover, content } = this.$refs, { left, top, options } = getPopoverposition( popover, content, this.placement ); // 有了坐标, 就能够很开心的定位了 this.left = left; this.top = top; // 这个配置是决定 '小三角' 的位置的 this.options = options; }); } } },
思路
vue-cc-ui/src/assets/js/vue-popper.js
// 受到vue源码实例化vue部分的启发, 有了以下写法. // CONTANT 常数: 物体距离目标的间隙距离, 单位px; function getPopoverPosition(popover, content, direction,CONTANT ) { // 这个show本次用不到, 为之后的组件作准备 let result = { show: true }; // 1: 让这个函数去初始化'参与运算的全部参数'; // 把处理好的值, 付给result对象 getOptions(result, popover, content, direction,CONTANT ); // 2: 拿到屏幕的偏移 let { left, top } = getScrollOffset(); // 3: return出去的坐标, 必定是针对当前可视区域的 result.left += left; result.top += top; return result; }
先把全部可能作成列表, 也许有人有疑问, 为何不把list这个组for循环生成, 那是由于for循环也是须要性能的, 这样直接下来能够减小运算, 因此不少不必的运算尽可能不要写
const list = [ 'top-end', 'left-end', 'top-start', 'right-end', 'top-middle', 'bottom-end', 'left-start', 'right-start', 'left-middle', 'right-middle', 'bottom-start', 'bottom-middle' ];
getOptions 初始化运算所需参数
function getOptions(result, popover, content, direction,CONTANT = 10) { // 1: 可能会反复的调用, 因此来个深复制 let myList = list.concat(), client = popover.getBoundingClientRect();// 获取popover的可视区距离 // 2: 每次使用一种模式, 就把这个模式从list中干掉, 这样直到数组为空, 就是全部可能性都尝试过了 myList.splice(list.indexOf(direction), 1); // 3: 把参数整理好, 传给处理函数 getDirection(result, { myList, direction, CONTANT, top: client.top, left: client.left, popoverWidth: popover.offsetWidth, contentWidth: content.offsetWidth, popoverHeight: popover.offsetHeight, contentHeight: content.offsetHeight }); }
getDirection
代码有点多, 可是逻辑很简单, 我来讲一下思路
function getDirection(result, options) { let { top, left, CONTANT, direction, contentWidth, popoverWidth, contentHeight, popoverHeight } = options; result.options = options; let main = direction.split('-')[0], around = direction.split('-')[1]; if (main === 'top' || main === 'bottom') { if (around === 'start') { result.left = left; } else if (around === 'end') { result.left = left + popoverWidth - contentWidth; } else if (around === 'middle') { result.left = left + popoverWidth / 2 - contentWidth / 2; } if (main === 'top') { result.top = top - contentHeight - CONTANT; } else { result.top = top + popoverHeight + CONTANT; } } else if (main === 'left' || main === 'right') { if (around === 'start') { result.top = top; } else if (around === 'end') { result.top = top + popoverHeight - contentHeight; } else if (around === 'middle') { result.top = top + popoverHeight / 2 - contentHeight / 2; } if (main === 'left') { result.left = left - contentWidth - CONTANT; } else { result.left = left + popoverWidth + CONTANT; } } testDirection(result, options); }
testDirection 检验算出来的值是否可以出如今用户的视野里面
思路
function testDirection(result, options) { let { left, top } = result, width = document.documentElement.clientWidth, height = document.documentElement.clientHeight; if ( top < 0 || left < 0 || top + options.contentHeight > height || left + options.contentWidth > width ) { // 还有能够循环的 if (options.myList.length) { options.direction = options.myList.shift(); getDirection(result, options); } else { // 实在不行就在父级身上 result.left = options.left; result.right = options.right; } } else { result.show = true; } }
dom结构上要相应的加上对应的样式
这里的click必定不能够用stop修饰符, 会干扰用户的正常操做.
这里咱们加上一个动画, 看起来渐隐渐现的有点美感.
<div class="cc-popover" ref='popover'> <!-- 不可使用stop 会阻止用户的操做 --> <transition name='fade'> <div v-if="init" ref='content' v-show='show' class="cc-popover__content" :class="options.direction" :style="{ // 这里就是控制定位的关键 top:top+'px', left:left+'px' }"> <div class="cc-popover__box"> <slot name="content"> 请输入内容</slot> </div> </div> </transition> <slot /> </div>
上面在watch里面也有体现了, 与click的区别就是, 绑定的事件不一样
这里消失有200毫秒的延迟, 是由于用户离开目标元素,多是为了移入popover弹出框
// 移入 handleMouseEnter() { clearTimeout(this.time); this.init = true; this.show = true; }, // 移出 handleMouseLeave() { clearTimeout(this.time); this.time = setTimeout(() => { this.show = false; }, 200); }
vue-cc-ui/src/components/Popover/main/index.js
思路
import Popover from './popover.vue'; import prevent from '@/assets/js/prevent'; Popover.install = function(Vue) { Vue.component(Popover.name, Popover); Vue.prototype.$clearPopover = function() { let ary = document.getElementsByClassName('cc-popover__content'); for (let i = 0; i < ary.length; i++) { ary[i].style.display = 'none'; } }; // 监听指令 window.addEventListener('scroll',()=>{ prevent(1,() => { Vue.prototype.$clearPopover() },400); },false) Vue.directive('scroll-clear-popover', { bind: el => { el.addEventListener('scroll', ()=>{ prevent(1,() => { Vue.prototype.$clearPopover() },400); }, false); } }); }; export default Popover;
不要小看这个, 若是没有这个收尾工做, 也许内存都爆了.
移除全部事件, 删除dom元素
beforeDestroy() { let { popover, content } = this.$refs; off(content, "mouseleave", this.handleMouseLeave); off(popover, "mouseleave", this.handleMouseLeave); off(content, "mouseenter", this.handleMouseEnter); off(popover, "mouseenter", this.handleMouseEnter); off(document, "click", this.close); document.body.removeChild(content); }
展现一下最终效果
你们均可以一块儿交流, 共同窗习,共同进步, 早日实现自我价值!!
下一集聊聊'日历组件'