Open Leecason opened 5 years ago
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();
},
};
// 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
<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>
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);
},
};
v-scroll 指令