vue 指令---氣泡提示(手擼實戰)
阿新 • • 發佈:2018-11-04
菜鳥學習之路
//L6zt github
自己在造元件輪子,也就是瞎搞。
自己寫了個slider元件,想加個氣泡提示。為了複用和省事特此寫了個指令來解決。
預覽地址
專案地址 github 我叫給它胡博
我對指令的理解: 前不久看過 一部分vnode實現原始碼,奈何資質有限...看不懂。
vnode的生命週期-----> 生成前、生成後、生成真正dom、更新 vnode、更新dom 、 銷燬。
而Vue的指令則是依賴於vnode 的生命週期, 無非也是有以上類似的鉤子。
程式碼效果
指令掛A元素上,預設生成一個氣泡容器B插入到 body 裡面,B 會獲取 元素 A 的位置資訊 和 自己的
大小資訊,經過 一些列的運算,B 元素會定位到 A 的 中間 上 位置。 當滑鼠放到 A 上 B 就會顯示出來,離開就會消失。
以下程式碼
氣泡指令
import { on , off , once, contains, elemOffset, position, addClass, removeClass } from '../utils/dom'; import Vue from 'vue' const global = window; const doc = global.document; const top = 15; export default { name : 'jc-tips' , bind (el , binding , vnode) { // 確定el 已經在頁面裡 為了獲取el 位置信信 Vue.nextTick(() => { const { context } = vnode; const { expression } = binding; // 氣泡元素根結點 const fWarpElm = doc.createElement('div'); // handleFn 氣泡裡的子元素(自定義) const handleFn = binding.expression && context[expression] || (() => ''); const createElm = handleFn(); fWarpElm.className = 'hide jc-tips-warp'; fWarpElm.appendChild(createElm); doc.body.appendChild(fWarpElm); // 給el 繫結元素待其他操作用 el._tipElm = fWarpElm; el._createElm = createElm; // 滑鼠放上去的 回撥函式 el._tip_hover_fn = function(e) { // 刪除節點函式 removeClass(fWarpElm, 'hide'); fWarpElm.style.opacity = 0; // 不加延遲 fWarpElm的大小資訊 (元素大小是 0 0)---> 刪除 class 不是立即渲染 setTimeout(() => { const offset = elemOffset(fWarpElm); const location = position(el); fWarpElm.style.cssText = `left: ${location.left - offset.width / 2}px; top: ${location.top - top - offset.height}px;`; fWarpElm.style.opacity = 1; }, 16); }; //滑鼠離開 元素 隱藏 氣泡 const handleLeave = function (e) { fWarpElm.style.opacity = 0; // transitionEnd 不太好應該加入相容 once({ el, type: 'transitionEnd', fn: function() { console.log('hide'); addClass(fWarpElm, 'hide'); } }) }; el._tip_leave_fn = handleLeave; // 解決 slider 移動結束 提示不消失 el._tip_mouse_up_fn = function (e) { const target = e.target; console.log(target); if (!contains(fWarpElm, target) && el !== target) { handleLeave(e) } }; on({ el, type: 'mouseenter', fn: el._tip_hover_fn }); on({ el, type: 'mouseleave', fn: el._tip_leave_fn }); on({ el: doc.body, type: 'mouseup', fn: el._tip_mouse_up_fn }) }); } , // 氣泡的資料變化 依賴於 context[expression] 返回的值 componentUpdated(el , binding , vnode) { const { context } = vnode; const { expression } = binding; const handleFn = expression && context[expression] || (() => ''); Vue.nextTick( () => { const createNode = handleFn(); const fWarpElm = el._tipElm; if (fWarpElm) { fWarpElm.replaceChild(createNode, el._createElm); el._createElm = createNode; const offset = elemOffset(fWarpElm); const location = position(el); fWarpElm.style.cssText = `left: ${location.left - offset.width / 2}px; top: ${location.top - top - offset.height}px;`; } }) }, // 刪除 事件 unbind (el , bind , vnode) { off({ el: dov.body, type: 'mouseup', fn: el._tip_mouse_up_fn }); el = null; } }
slider 元件
<template> <div class="jc-slider-cmp"> <section class="slider-active-bg" :style="{width: `${left}px`}" > </section> <i class="jc-slider-dot" :style="{left: `${left}px`}" ref="dot" @mousedown="moveStart" v-jc-tips="createNode" > </i> </div> </template> <script> import {elemOffset, on, off, once} from "../../utils/dom"; const global = window; const doc = global.document; export default { props: { step: { type: [Number], default: 0 }, rangeEnd: { type: [Number], required: true }, value: { type: [Number], required: true }, minValue: { type: [Number], required: true }, maxValue: { type: [Number], required: true } }, data () { return { startX: null, width: null, curValue: 0, curStep: 0, left: 0, tempLeft: 0 } }, computed: { wTov () { let step = this.step; let width = this.width; let rangeEnd = this.rangeEnd; if (width) { if (step) { return width / (rangeEnd / step) } return width / rangeEnd } return null }, postValue () { let value = null; if (this.step) { value = this.step * this.curStep; } else { value = this.left / this.wTov; } return value; } }, watch: { value: { handler (value) { this.$nextTick(() => { let step = this.step; let wTov = this.wTov; if (step) { this.left = value / step * wTov; } else { this.left = value * wTov; } }) }, immediate: true } }, methods: { moveStart (e) { e.preventDefault(); const body = window.document.body; const _this = this; this.startX = e.pageX; this.tempLeft = this.left; on({ el: body, type: 'mousemove', fn: this.moving }); once({ el: body, type: 'mouseup', fn: function() { console.log('end'); _this.$emit('input', _this.postValue); off({ el: body, type: 'mousemove', fn: _this.moving }) } }) }, moving(e) { let curX = e.pageX; let step = this.step; let rangeEnd = this.rangeEnd; let width = this.width; let tempLeft = this.tempLeft; let startX = this.startX; let wTov = this.wTov; if (step !== 0) { let all = parseInt(rangeEnd / step); let curStep = (tempLeft + curX - startX) / wTov; curStep > all && (curStep = all); curStep < 0 && (curStep = 0); curStep = Math.round(curStep); this.curStep = curStep; this.left = curStep * wTov; } else { let left = tempLeft + curX - startX; left < 0 && (left = 0); left > width && (left = width); this.left = left; } }, createNode () { const fElem = document.createElement('i'); const textNode = document.createTextNode(this.postValue.toFixed(2)); fElem.appendChild(textNode); return fElem; } }, mounted () { this.width = elemOffset(this.$el).width; } }; </script> <style lang="scss"> .jc-slider-cmp { position: relative; display: inline-block; width: 100%; border-radius: 4px; height: 8px; background: #ccc; .jc-slider-dot { position: absolute; display: inline-block; width: 15px; height: 15px; border-radius: 50%; left: 0; top: 50%; transform: translate(-50%, -50%); background: #333; cursor: pointer; } .slider-active-bg { position: relative; height: 100%; border-radius: 4px; background: red; } } </style>
../utils/dom
const global = window;
export const on = ({el, type, fn}) => {
if (typeof global) {
if (global.addEventListener) {
el.addEventListener(type, fn, false)
} else {
el.attachEvent(`on${type}`, fn)
}
}
};
export const off = ({el, type, fn}) => {
if (typeof global) {
if (global.removeEventListener) {
el.removeEventListener(type, fn)
} else {
el.detachEvent(`on${type}`, fn)
}
}
};
export const once = ({el, type, fn}) => {
const hyFn = (event) => {
try {
fn(event)
}
finally {
off({el, type, fn: hyFn})
}
}
on({el, type, fn: hyFn})
};
// 最後一個
export const fbTwice = ({fn, time = 300}) => {
let [cTime, k] = [null, null]
// 獲取當前時間
const getTime = () => new Date().getTime()
// 混合函式
const hyFn = () => {
const ags = argments
return () => {
clearTimeout(k)
k = cTime = null
fn(...ags)
}
};
return () => {
if (cTime == null) {
k = setTimeout(hyFn(...arguments), time)
cTime = getTime()
} else {
if ( getTime() - cTime < 0) {
// 清除之前的函式堆 ---- 重新記錄
clearTimeout(k)
k = null
cTime = getTime()
k = setTimeout(hyFn(...arguments), time)
}
}}
};
export const contains = function(parentNode, childNode) {
if (parentNode.contains) {
return parentNode !== childNode && parentNode.contains(childNode)
} else {
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/compareDocumentPosition
return (parentNode.compareDocumentPosition(childNode) === 16)
}
};
export const addClass = function (el, className) {
if (typeof el !== "object") {
return null
}
let classList = el['className']
classList = classList === '' ? [] : classList.split(/\s+/)
if (classList.indexOf(className) === -1) {
classList.push(className)
el.className = classList.join(' ')
}
};
export const removeClass = function (el, className) {
let classList = el['className']
classList = classList === '' ? [] : classList.split(/\s+/)
classList = classList.filter(item => {
return item !== className
})
el.className = classList.join(' ')
};
export const delay = ({fn, time}) => {
let oT = null
let k = null
return () => {
// 當前時間
let cT = new Date().getTime()
const fixFn = () => {
k = oT = null
fn()
}
if (k === null) {
oT = cT
k = setTimeout(fixFn, time)
return
}
if (cT - oT < time) {
oT = cT
clearTimeout(k)
k = setTimeout(fixFn, time)
}
}
};
export const position = (son, parent = global.document.body) => {
let top = 0;
let left = 0;
let offsetParent = son;
while (offsetParent !== parent) {
let dx = offsetParent.offsetLeft;
let dy = offsetParent.offsetTop;
let old = offsetParent;
if (dx === null) {
return {
flag: false
}
}
left += dx;
top += dy;
offsetParent = offsetParent.offsetParent;
if (offsetParent === null && old !== global.document.body) {
return {
flag: false
}
}
}
return {
flag: true,
top,
left
}
};
export const getElem = (filter) => {
return Array.from(global.document.querySelectorAll(filter));
};
export const elemOffset = (elem) => {
return {
width: elem.offsetWidth,
height: elem.offsetHeight
}
};
原文地址: https://segmentfault.com/a/1190000016719995