v-track
Version:
一个基于Vue指令的埋点插件
184 lines (157 loc) • 4.12 kB
JavaScript
/*
* @Author: 宋慧武
* @Date: 2019-04-08 11:13:34
* @Last Modified by: 宋慧武
* @Last Modified time: 2020-06-07 10:47:44
*/
import { isElement, isVisible, isInViewport } from "./dom";
import { isFun, debounce } from "./helper";
/**
* @class
* @name VisMonitor
*
* @desc 目标元素控制器
*/
export default class VisMonitor {
constructor(ele, ref, refwin = window, percent = 1) {
if (!isElement(ele)) {
throw new Error("not an element node");
}
if (percent > 1 && percent <= 0) {
throw new Error("'percent' must be a number between 0 and 1");
}
this.ele = ele;
this.ref = ref;
this.refWin = refwin;
this.started = false;
this.percent = percent;
this.prevPerc = null; // 保存前一次曝光百分比
this.listeners = {};
this.removeScrollLisener = null;
this.init();
}
init() {
if (!this.started) {
const listener = debounce(this.visibilitychange.bind(this));
listener();
this.removeScrollLisener = (listener => {
if (this.ref) {
return this.ref.$on("scroll", listener);
} else {
this.refWin.addEventListener("scroll", listener, true);
return () =>
this.refWin.removeEventListener("scroll", listener, true);
}
})(listener);
this.started = true;
}
}
viewport() {
const win = this.refWin;
const rect = isElement(win) ? win.getBoundingClientRect() : win;
return {
top: isElement(win) ? rect.top : 0,
right: rect.right || rect.innerWidth,
bottom: rect.bottom || rect.innerHeight,
left: rect.left || 0,
height: win.innerHeight || win.offsetHeight,
width: win.innerWidth || win.offsetWidth
};
}
/**
* 监听自定义事件
*/
$on(evt, cbk) {
const queue = this.listeners[evt] || (this.listeners[evt] = []);
queue.push(cbk);
return this;
}
/**
* 移除监听自定义事件
*/
$off(evt, cbk) {
if (!cbk) return;
let queue = this.listeners[evt];
let v;
let i = queue.length;
while (i--) {
v = queue[i];
if (v === cbk || v.cbk === cbk) {
queue.splice(i, 1);
break;
}
}
return this;
}
/**
* 监听自定义事件,但只触发一次
*/
$once(evt, cbk) {
const on = (...args) => {
this.$off(evt, on);
cbk.apply(this, args);
};
on.cbk = cbk;
this.$on(evt, on);
return this;
}
/**
* 触发当前实例的监听回调
*/
$emit(evt, ...args) {
const queue = this.listeners[evt] || [];
queue.forEach(sub => sub.apply(this, args));
return this;
}
/**
* 计算元素可见比例,如果比例为100%,则触发 fullyvisible 事件
*/
visibilitychange() {
const rect = this.ele.getBoundingClientRect();
const view = this.viewport();
if (!isInViewport(rect, view) || !isVisible(this.ele)) {
this.prevPerc = 0;
return 0;
}
let vh = 0;
let vw = 0;
let perc = 0;
if (view.top < 0) {
view.top = 0;
}
if (view.bottom > window.innerHeight) {
view.bottom = window.innerHeight;
}
if (view.left < 0) {
view.left = 0;
}
if (view.right > window.innerWidth) {
view.right = window.innerWidth;
}
if (rect.top >= view.top && rect.bottom > view.bottom) {
vh = view.bottom - rect.top;
} else if (rect.top < view.top && rect.bottom <= view.bottom) {
vh = rect.bottom - view.top;
} else {
vh = rect.height;
}
if (rect.left >= view.left && rect.right > view.right) {
vw = view.right - rect.left;
} else if (rect.left < view.left && rect.right <= view.right) {
vw = rect.right - view.left;
} else {
vw = rect.width;
}
perc = (vh * vw) / (rect.height * rect.width);
if (this.prevPerc < this.percent && perc >= this.percent) {
this.$emit("fullyvisible");
this.prevPerc = perc;
}
}
/**
* 销毁当前实例的事件
*/
destroy() {
isFun(this.removeScrollLisener) && this.removeScrollLisener();
}
}