UNPKG

my-test-intersection-observer

Version:
326 lines (265 loc) 11.5 kB
(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 }); }));