my-test-intersection-observer
Version:
IntersectionObserver for hippy-vue
326 lines (265 loc) • 11.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue'), require('lodash/throttle')) :
typeof define === 'function' && define.amd ? define(['exports', 'vue', 'lodash/throttle'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.HippyVueIntersectionObserver = {}, global.Vue, global.throttle));
})(this, (function (exports, Vue, throttle) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var Vue__default = /*#__PURE__*/_interopDefaultLegacy(Vue);
var throttle__default = /*#__PURE__*/_interopDefaultLegacy(throttle);
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var Bus = new Vue__default["default"]();
function restrictValueInRange() {
var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var value = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
return Math.min(Math.max(start, value), end);
}
function getLastMatchedThreshold(value, thresholds) {
var matchedThreshold = 0;
for (var i = 0; i < thresholds.length; i += 1) {
if (value >= thresholds[i]) {
matchedThreshold = thresholds[i];
}
}
return matchedThreshold;
}
var IntersectionObserverEvent = 'IntersectionObserverEvent';
var kDefaultRootMargin = {
left: 0,
right: 0,
top: 0,
bottom: 0
};
var kDefaultThresholds = [0];
var kDefaultThrottle = 300;
var HippyIntersectionObserver = /*#__PURE__*/_createClass(function HippyIntersectionObserver(app, scope, callback, options) {
var _this = this;
_classCallCheck(this, HippyIntersectionObserver);
this.targets = [];
this.previousIntersectionRatios = new Map(); // 前一次的相交比例,决定是否回调
/**
* 开始监听指定元素
* @param target 目标元素
*/
this.observe = function (target) {
var index = _this.targets.indexOf(target);
if (index < 0) {
_this.targets.push(target);
_this.measureTarget(target).then(function (targetMeasureResult) {
_this.callback([targetMeasureResult]);
_this.previousIntersectionRatios.set(target, targetMeasureResult.intersectionRatio);
});
}
if (_this.targets.length > 0) {
_this.app.$on(IntersectionObserverEvent, throttle__default["default"](_this.handleEmitterEvent, _this.throttle));
}
};
/**
* 停止监听指定元素
* @param target 目标元素
*/
this.unobserve = function (target) {
var index = _this.targets.indexOf(target);
if (index >= 0) {
_this.targets.splice(index, 1);
}
if (_this.targets.length <= 0) {
// todo: check is null ok?
_this.app.$off(IntersectionObserverEvent, null);
}
};
this.measureTarget = function (target) {
return new Promise(function (resolve) {
Vue__default["default"].Native.measureInWindow(target).then(function (measureResult) {
var boundingClientRect = {
left: measureResult.left,
top: measureResult.top,
right: measureResult.right,
bottom: measureResult.bottom
};
console.log('debug: measure in window result:');
console.log(boundingClientRect);
var _this$measureIntersec = _this.measureIntersection(boundingClientRect),
intersectionRatio = _this$measureIntersec.intersectionRatio,
intersectionRect = _this$measureIntersec.intersectionRect;
var isIntersecting = _this.isIntersecting(intersectionRatio);
var targetMeasureResult = {
boundingClientRect: boundingClientRect,
intersectionRatio: intersectionRatio,
intersectionRect: intersectionRect,
target: target,
isIntersecting: isIntersecting
};
resolve(targetMeasureResult);
});
});
};
this.handleEmitterEvent = function (params) {
if (params.scope && params.scope.length && _this.scope && _this.scope.length && params.scope !== _this.scope) {
return;
}
var measureTasks = _this.targets.map(function (target) {
return _this.measureTarget(target);
});
Promise.all(measureTasks).then(function (measureResults) {
var needReportEntries = measureResults.filter(function (measureResult) {
var previousRatio = _this.previousIntersectionRatios.get(measureResult.target);
return _this.needReportIntersection(measureResult.intersectionRatio, previousRatio);
});
measureResults.forEach(function (measureResult) {
_this.previousIntersectionRatios.set(measureResult.target, measureResult.intersectionRatio);
});
if (needReportEntries && needReportEntries.length > 0) {
_this.callback(needReportEntries);
}
});
};
this.isIntersecting = function (intersectionRatio) {
var minIntersectionThreshold = Math.max(Math.min.apply(Math, _toConsumableArray(_this.thresholds)), 0);
return intersectionRatio >= minIntersectionThreshold;
};
this.needReportIntersection = function (ratio) {
var previousRatio = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
console.log("debug: intersection ratio:".concat(ratio, ", previous:").concat(previousRatio));
if (_this.isIntersecting(ratio) !== _this.isIntersecting(previousRatio)) {
return true;
}
return getLastMatchedThreshold(ratio, _this.thresholds) !== getLastMatchedThreshold(previousRatio, _this.thresholds);
};
this.measureIntersection = function (boundingClientRect) {
var window = Vue__default["default"].Native.Dimensions.window;
var pageHeight = window.height;
var pageWidth = window.width; // 计算屏幕可见区域
var displayAreaTop = _this.rootMargin.top;
var displayAreaBottom = pageHeight - _this.rootMargin.bottom;
var displayAreaLeft = _this.rootMargin.left;
var displayAreaRight = pageWidth - _this.rootMargin.right; // 计算目标元素可视区域
var visibleTop = restrictValueInRange(displayAreaTop, displayAreaBottom, boundingClientRect.top);
var visibleBottom = restrictValueInRange(displayAreaTop, displayAreaBottom, boundingClientRect.bottom);
var visibleLeft = restrictValueInRange(displayAreaLeft, displayAreaRight, boundingClientRect.left);
var visibleRight = restrictValueInRange(displayAreaLeft, displayAreaRight, boundingClientRect.right); // 计算两个区域的面积
var itemArea = (boundingClientRect.bottom - boundingClientRect.top) * (boundingClientRect.right - boundingClientRect.left);
var visibleArea = (visibleBottom - visibleTop) * (visibleRight - visibleLeft);
var intersectionRect = {
top: visibleTop,
bottom: displayAreaBottom,
left: visibleLeft,
right: displayAreaRight
};
var intersectionRatio = itemArea ? visibleArea / itemArea : 0;
var intersectionRatioRound = Math.round(intersectionRatio * 100) / 100;
return {
intersectionRect: intersectionRect,
intersectionRatio: intersectionRatioRound
};
};
this.app = app;
this.scope = scope;
this.rootMargin = options && options.rootMargin || kDefaultRootMargin;
this.thresholds = options && options.thresholds || kDefaultThresholds;
this.throttle = options && options.throttle || kDefaultThrottle;
this.thresholds.sort();
this.callback = callback;
});
/**
* 触发IntersectionObserver检测,通常在onScroll时机进行触发
* @param scope
*/
function HippyIntersectionEmitEvent(scope) {
var emitFunc = throttle__default["default"](function (scope) {
Bus.$emit(IntersectionObserverEvent, {
scope: scope
});
}, 50);
return emitFunc(scope);
}
var script = {
name: 'hippy-vue-observer',
props: ['scope', 'rootMargin', 'thresholds', 'throttle'],
data() {
return {
observer: null,
isMounted: false,
};
},
mounted() {
const options = {
rootMargin: this.rootMargin,
thresholds: this.thresholds,
throttle: this.throttle,
};
const callback = (entries) => {
entries.forEach((entry) => {
this.$emit('on-change', entry);
});
};
this.observer = new HippyIntersectionObserver(Bus, this.scope, callback, options);
this.observer.observe(this.$slots.default[0].elm);
this.isMounted = true;
},
beforeDestroy() {
this.observer.unobserve(this.$slots.default[0].elm);
this.isMounted = false;
},
render(h) {
return h('div', { collapsable: 'false' }, this.$slots.default);
},
};
script.__file = "src/components/hippy-vue-observer.vue";
var install = function install(Vue) {
Vue.component(script.name, script);
};
var plugin = {
install: install,
HippyVueObserver: script
};
exports.HippyIntersectionEmitEvent = HippyIntersectionEmitEvent;
exports.HippyVueObserver = plugin;
Object.defineProperty(exports, '__esModule', { value: true });
}));