Leecason / blog

https://leecason.github.io
1 stars 0 forks source link

Vue 相关的组件、指令等 #16

Open Leecason opened 5 years ago

Leecason commented 5 years ago

v-scroll 指令

// https://github.com/Leecason/blog/issues/1#issuecomment-530672735
import getScrollParent from '...';

function bindScroll (el, binding) {
  const callback = typeof binding.value === 'function'
    ? binding.value
    : binding.value.callback;

  let { target } = binding.value;
  if (target instanceof Element) {
    target = getScrollParent(target);
  } else if (typeof target === 'string') {
    target = document.querySelector(target);
  }

  if (!target) target = window;

  if (el.__scroll__ && target !== el.__scroll__.target) unbind(el, binding);

  target.addEventListener('scroll', callback, { passive: true });

  el.__scroll__ = {
    callback,
    target,
  };
}

function unbind (el, _binding) {
  const { callback, target } = el.__scroll__;
  if (!target) return;

  target.removeEventListener('scroll', callback);
}

export default {
  name: 'scroll',

  inserted: bindScroll,

  update: bindScroll,

  unbind,
};
Leecason commented 5 years ago

v-resize 指令

import _ from 'lodash';

const DEFAULT_DEBOUNCE = 0,
      DEFAULT_THROTTLE = 200,
      DEFAULT_IMMEDIATE = false;

class Resize {
  constructor (el, options) {
    this.el = el;

    this.initOptions(options);
    this.bindEvent();
    this.onLoaded();
  }

  initOptions (opts) {
    let options;

    if (typeof opts === 'function') {
      options = {
        cb: opts,
        debounce: DEFAULT_DEBOUNCE,
        throttle: DEFAULT_THROTTLE,
        immediate: DEFAULT_IMMEDIATE,
      };
    } else {
      options = {
        cb: opts.callback,
        debounce: opts.debounce || DEFAULT_DEBOUNCE,
        throttle: opts.throttle || DEFAULT_IMMEDIATE,
        immediate: opts.immediate || DEFAULT_IMMEDIATE,
      };
    }

    this.options = options;
  }

  bindEvent () {
    const { cb, debounce, throttle } = this.options;

    const debounced_cb = _.debounce(cb, debounce);
    this.__cb__ = _.throttle(debounced_cb, throttle);

    window.addEventListener('resize', this.__cb__, { passive: true });
  }

  unbindEvent () {
    window.removeEventListener('resize', this.__cb__);
  }

  onLoaded () {
    const { cb, immediate } = this.options;

    immediate && cb();
  }
}

export default {
  name: 'resize',

  inserted (el, binding) {
    el.__resize__ = new Resize(el, binding.value);
  },

  unbind (el, _binding) {
    el.__resize__.unbindEvent();
  },
};
Leecason commented 5 years ago

Tooltip 组件

tooltip.vue

// tooltip.vue
<script>
import _ from 'lodash';
import Vue from 'vue';

import TooltipContent from './tooltip_content.vue';

export default {
  name: 'Tooltip',

  props: {
    content: {
      type: String,
      default: '',
    },

    placement: {
      type: String,
      default: 'top-center',
      validator (placement) {
        return [
          'top-center', 'top-start', 'top-end',
          'bottom-center', 'bottom-start', 'bottom-end',
        ].indexOf(placement) !== -1;
      },
    },

    opened: {
      type: Boolean,
      default: false,
    },

    delay: {
      type: Number,
      default: 0,
    },

    tooltipClass: {
      type: [String, Array, Object],
      default: '',
    },
  },

  data () {
    return {
      active: this.opened,
      trigger: null,
    };
  },

  watch: {
    active (val) {
      this.$emit('update:opened', val);
    },

    opened (opened) {
      this.active = opened;
    },
  },

  beforeCreate () {
    this.tooltipVm = new Vue({
      data: {
        node: '',
      },
      render (_h) {
        return this.node;
      },
    }).$mount();
  },

  mounted () {
    this.trigger = this.$el;
  },

  beforeDestroy () {
    this.tooltipVm.$destroy();
  },

  methods: {
    show () {
      if (this.delay > 0) {
        this.timer = setTimeout(() => {
          this.active = true;
        }, this.delay);
      } else {
        this.active = true;
      }
    },

    hide () {
      if (this.timer) clearTimeout(this.timer);
      this.active = false;
    },
  },

  render (h) {
    const content = (this.$slots.content ? this.$slots.content : this.content) || '';

    if (this.tooltipVm) {
      this.tooltipVm.node = h(TooltipContent, {
        class: this.tooltipClass,
        props: {
          placement: this.placement,
          opened: this.active,
          trigger: this.trigger,
        },
      }, content);

      const vnode = this.$slots.default[0];
      if (!vnode) return vnode;
      vnode.data = vnode.data || {};

      _.extend(vnode.data.on = vnode.data.on || {}, {
        mouseenter: this.show,
        mouseleave: this.hide,
      });

      return vnode;
    }
  },
};
</script>

tooltip_content.vue

// tooltip_content.vue
<script>
import scroll from '...'; // https://github.com/Leecason/blog/issues/16#issuecomment-530675005
import resize from '...', // https://github.com/Leecason/blog/issues/16#issuecomment-530675454

const SPACE = 10; // 4 + 6: caret-height + caret-space

export default {
  name: 'TooltipContent',

  directives: {
    scroll,
    resize,
  },

  props: {
    opened: {
      type: Boolean,
      default: false,
    },

    placement: {
      type: String,
      default: 'top-center',
      validator (placement) {
        return [
          'top-center', 'top-start', 'top-end',
          'bottom-center', 'bottom-start', 'bottom-end',
        ].indexOf(placement) !== -1;
      },
    },

    trigger: {}, // HTML element | vm
  },

  data () {
    return {
      appended: false,
    };
  },

  mounted () {
    this.setStyle();
  },

  updated () {
    this.$nextTick(() => this.setStyle());
  },

  beforeDestroy () {
    if (!this.$el) return;
    if (this.$el.parentNode) this.$el.parentNode.removeChild(this.$el);
  },

  methods: {
    appendElToBody () {
      if (this.appended) return;

      document.body.appendChild(this.$el);
      this.appended = true;
    },

    setStyle () {
      if (!this.opened) return;
      this.appendElToBody();

      const el = this.$el;
      const trigger_el = this.trigger;
      if (!el || !trigger_el) return;
      const el_rect = el.getBoundingClientRect();
      const trigger_rect = trigger_el.getBoundingClientRect();
      el.style.top = `${this._getTopPosition(el_rect.height, trigger_rect)}px`;
      el.style.left = `${this._getLeftPosition(el_rect.width, trigger_rect)}px`;
    },

    _getTopPosition (height, rect) {
      switch (this.placement) {
        case 'top-center':
        case 'top-start':
        case 'top-end':
          return rect.top - height - SPACE;
        case 'bottom-center':
        case 'bottom-start':
        case 'bottom-end':
          return rect.top + rect.height + SPACE;
      }
    },

    _getLeftPosition (width, rect) {
      switch (this.placement) {
        case 'top-center':
        case 'bottom-center':
          return rect.left + (rect.width / 2) - (width / 2);
        case 'bottom-start':
        case 'top-start':
          return rect.left;
        case 'bottom-end':
        case 'top-end':
          return (rect.left + rect.width) - width;
      }
    },
  },

  render (h) {
    return h('transition', {
      props: {
        name: 'tooltip',
      },
    }, [
      this.opened ? h('div', {
        staticClass: 'tooltip',
        class: `tooltip__${this.placement}`,
        attrs: {
          placement: this.placement,
        },
        directives: [
          {
            name: 'scroll',
            value: {
              callback: this.setStyle,
              target: this.trigger,
            },
           {
             name: 'resize',
             value: this.setStyle,
           },
          },
        ],
      }, this.$slots.default) : null,
    ]);
  },
};
</script>
Leecason commented 5 years ago

v-click-outside 指令

export default {
  name: 'click-outside',

  bind (el, binding, vnode) {
    const documentHandler = function (event) {
      if (!vnode.context || el.contains(event.target)) return;

      if (binding.expression) {
        vnode.context[el.__clickOutside__.methodName](event);
      } else {
        el.__clickOutside__.bindingFn(event);
      }
    };
    el.__clickOutside__ = {
      documentHandler,
      methodName: binding.expression,
      bindingFn: binding.value
    };
    setTimeout(() => {
      document.addEventListener('click', documentHandler);
    }, 0);
  },

  update (el, binding) {
    el.__clickOutside__.methodName = binding.expression;
    el.__clickOutside__.bindingFn = binding.value;
  },

  unbind (el) {
    document.removeEventListener('click', el.__clickOutside__.documentHandler);
  },
};