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); }
展現一下最終效果
你們均可以一塊兒交流, 共同窗習,共同進步, 早日實現自我價值!!
下一集聊聊'日曆組件'