vue 指令---气泡提示(手撸实战)


菜鸟学习之路
//L6zt github
自己在造组件轮子,也就是瞎搞。
自己写了个slider组件,想加个气泡提示。为了复用和省事特此写了个指令来解决。
预览地址
项目地址 github 我叫给它胡博

css图形制作

我对指令的理解: 前不久看过 一部分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

posted @ 2018-11-04 23:12  sfornt  阅读(3998)  评论(0编辑  收藏  举报