UNPKG

vue-virtual-scroller

Version:
977 lines (864 loc) 27.9 kB
function getInternetExplorerVersion() { var ua = window.navigator.userAgent; var msie = ua.indexOf('MSIE '); if (msie > 0) { // IE 10 or older => return version number return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); } var trident = ua.indexOf('Trident/'); if (trident > 0) { // IE 11 => return version number var rv = ua.indexOf('rv:'); return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); } var edge = ua.indexOf('Edge/'); if (edge > 0) { // Edge (IE 12+) => return version number return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); } // other browser return -1; } var isIE = void 0; function initCompat() { if (!initCompat.init) { initCompat.init = true; isIE = getInternetExplorerVersion() !== -1; } } var ResizeObserver = { render: function render() { var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('div', { staticClass: "resize-observer", attrs: { "tabindex": "-1" } }); }, staticRenderFns: [], _scopeId: 'data-v-b329ee4c', name: 'resize-observer', methods: { notify: function notify() { this.$emit('notify'); }, addResizeHandlers: function addResizeHandlers() { this._resizeObject.contentDocument.defaultView.addEventListener('resize', this.notify); if (this._w !== this.$el.offsetWidth || this._h !== this.$el.offsetHeight) { this.notify(); } }, removeResizeHandlers: function removeResizeHandlers() { if (this._resizeObject && this._resizeObject.onload) { if (!isIE && this._resizeObject.contentDocument) { this._resizeObject.contentDocument.defaultView.removeEventListener('resize', this.notify); } delete this._resizeObject.onload; } } }, mounted: function mounted() { var _this = this; initCompat(); this.$nextTick(function () { _this._w = _this.$el.offsetWidth; _this._h = _this.$el.offsetHeight; }); var object = document.createElement('object'); this._resizeObject = object; object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); object.setAttribute('aria-hidden', 'true'); object.onload = this.addResizeHandlers; object.type = 'text/html'; if (isIE) { this.$el.appendChild(object); } object.data = 'about:blank'; if (!isIE) { this.$el.appendChild(object); } }, beforeDestroy: function beforeDestroy() { this.removeResizeHandlers(); } }; // Install the components function install(Vue) { Vue.component('resize-observer', ResizeObserver); /* -- Add more components here -- */ } /* -- Plugin definition & Auto-install -- */ /* You shouldn't have to modify the code below */ // Plugin var plugin = { // eslint-disable-next-line no-undef version: "0.4.3", install: install }; // Auto-install var GlobalVue = null; if (typeof window !== 'undefined') { GlobalVue = window.Vue; } else if (typeof global !== 'undefined') { GlobalVue = global.Vue; } if (GlobalVue) { GlobalVue.use(plugin); } function throwValueError(value) { if (value !== null && typeof value !== 'function') { throw new Error('observe-visibility directive expects a function as the value'); } } var ObserveVisibility = { bind: function bind(el, _ref, vnode) { var value = _ref.value; if (typeof IntersectionObserver === 'undefined') { console.warn('[vue-observe-visibility] IntersectionObserver API is not available in your browser. Please install this polyfill: https://github.com/WICG/IntersectionObserver/tree/gh-pages/polyfill'); } else { throwValueError(value); el._vue_visibilityCallback = value; var observer = el._vue_intersectionObserver = new IntersectionObserver(function (entries) { var entry = entries[0]; if (el._vue_visibilityCallback) { el._vue_visibilityCallback.call(null, entry.intersectionRatio > 0, entry); } }); // Wait for the element to be in document vnode.context.$nextTick(function () { observer.observe(el); }); } }, update: function update(el, _ref2) { var value = _ref2.value; throwValueError(value); el._vue_visibilityCallback = value; }, unbind: function unbind(el) { if (el._vue_intersectionObserver) { el._vue_intersectionObserver.disconnect(); delete el._vue_intersectionObserver; delete el._vue_visibilityCallback; } } }; // Install the components function install$1(Vue) { Vue.directive('observe-visibility', ObserveVisibility); /* -- Add more components here -- */ } /* -- Plugin definition & Auto-install -- */ /* You shouldn't have to modify the code below */ // Plugin var plugin$2 = { // eslint-disable-next-line no-undef version: "0.3.1", install: install$1 }; // Auto-install var GlobalVue$1 = null; if (typeof window !== 'undefined') { GlobalVue$1 = window.Vue; } else if (typeof global !== 'undefined') { GlobalVue$1 = global.Vue; } if (GlobalVue$1) { GlobalVue$1.use(plugin$2); } var supportsPassive = false; if (typeof window !== 'undefined') { supportsPassive = false; try { var opts = Object.defineProperty({}, 'passive', { get: function get() { supportsPassive = true; } }); window.addEventListener('test', null, opts); } catch (e) {} } // @vue/component var Scroller = { components: { ResizeObserver: ResizeObserver }, directives: { ObserveVisibility: ObserveVisibility }, props: { items: { type: Array, required: true }, itemHeight: { type: [Number, String], default: null }, minItemHeight: { type: [Number, String], default: null }, heightField: { type: String, default: 'height' }, typeField: { type: String, default: 'type' }, buffer: { type: [Number, String], default: 200 }, pageMode: { type: Boolean, default: false }, prerender: { type: [Number, String], default: 0 }, emitUpdate: { type: Boolean, default: false } }, computed: { cssClass: function cssClass() { return { 'page-mode': this.pageMode }; }, heights: function heights() { if (this.itemHeight === null) { var heights = { '-1': { accumulator: 0 } }; var items = this.items; var field = this.heightField; var minItemHeight = this.minItemHeight; var accumulator = 0; var current = void 0; for (var i = 0, l = items.length; i < l; i++) { current = items[i][field] || minItemHeight; accumulator += current; heights[i] = { accumulator: accumulator, height: current }; } return heights; } } }, beforeDestroy: function beforeDestroy() { this.removeWindowScroll(); }, methods: { getScroll: function getScroll() { var el = this.$el; var scroll = void 0; if (this.pageMode) { var rect = el.getBoundingClientRect(); var top = -rect.top; var height = window.innerHeight; if (top < 0) { height += top; top = 0; } if (top + height > rect.height) { height = rect.height - top; } scroll = { top: top, bottom: top + height }; } else { scroll = { top: el.scrollTop, bottom: el.scrollTop + el.clientHeight }; } if (scroll.bottom >= 0 && scroll.top <= scroll.bottom) { return scroll; } else { return null; } }, applyPageMode: function applyPageMode() { if (this.pageMode) { this.addWindowScroll(); } else { this.removeWindowScroll(); } }, addWindowScroll: function addWindowScroll() { window.addEventListener('scroll', this.handleScroll, supportsPassive ? { passive: true } : false); window.addEventListener('resize', this.handleResize); }, removeWindowScroll: function removeWindowScroll() { window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.handleResize); }, scrollToItem: function scrollToItem(index) { var scrollTop = void 0; if (this.itemHeight === null) { scrollTop = index > 0 ? this.heights[index - 1].accumulator : 0; } else { scrollTop = index * this.itemHeight; } this.scrollToPosition(scrollTop); }, scrollToPosition: function scrollToPosition(position) { this.$el.scrollTop = position; } } }; var VirtualScroller = { render: function render() { var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c(_vm.mainTag, { directives: [{ name: "observe-visibility", rawName: "v-observe-visibility", value: _vm.handleVisibilityChange, expression: "handleVisibilityChange" }], tag: "component", staticClass: "virtual-scroller", class: _vm.cssClass, on: { "&scroll": function scroll($event) { _vm.handleScroll($event); } } }, [_vm._t("before-container"), _vm._v(" "), _c(_vm.containerTag, { ref: "itemContainer", tag: "component", staticClass: "item-container", class: _vm.containerClass, style: _vm.itemContainerStyle }, [_vm._t("before-content"), _vm._v(" "), _c(_vm.contentTag, { ref: "items", tag: "component", staticClass: "items", class: _vm.contentClass, style: _vm.itemsStyle }, [_vm.renderers ? _vm._l(_vm.visibleItems, function (item, index) { return _c(_vm.renderers[item[_vm.typeField]], { key: _vm.keysEnabled && item[_vm.keyField] || undefined, tag: "component", staticClass: "item", attrs: { "item": item, "item-index": _vm.$_startIndex + index } }); }) : [_vm._l(_vm.visibleItems, function (item, index) { return _vm._t("default", null, { item: item, itemIndex: _vm.$_startIndex + index, itemKey: _vm.keysEnabled && item[_vm.keyField] || undefined }); })]], 2), _vm._v(" "), _vm._t("after-content")], 2), _vm._v(" "), _vm._t("after-container"), _vm._v(" "), _c('resize-observer', { on: { "notify": _vm.handleResize } })], 2); }, staticRenderFns: [], _scopeId: 'data-v-2b1f2e05', name: 'virtual-scroller', mixins: [Scroller], props: { renderers: { default: null }, keyField: { type: String, default: 'id' }, mainTag: { type: String, default: 'div' }, containerTag: { type: String, default: 'div' }, containerClass: { default: null }, contentTag: { type: String, default: 'div' }, contentClass: { default: null }, poolSize: { type: [Number, String], default: 2000 }, delayPreviousItems: { type: Boolean, default: false } }, data: function data() { return { visibleItems: [], itemContainerStyle: null, itemsStyle: null, keysEnabled: true }; }, watch: { items: { handler: function handler() { this.updateVisibleItems(true); }, deep: true }, pageMode: function pageMode() { this.applyPageMode(); this.updateVisibleItems(true); }, itemHeight: 'setDirty' }, created: function created() { this.$_ready = false; this.$_startIndex = 0; this.$_oldScrollTop = null; this.$_oldScrollBottom = null; this.$_offsetTop = 0; this.$_height = 0; this.$_scrollDirty = false; this.$_updateDirty = false; var prerender = parseInt(this.prerender); if (prerender > 0) { this.visibleItems = this.items.slice(0, prerender); this.$_length = this.visibleItems.length; this.$_endIndex = this.$_length - 1; this.$_skip = true; } else { this.$_endIndex = 0; this.$_length = 0; this.$_skip = false; } }, mounted: function mounted() { var _this = this; this.applyPageMode(); this.$nextTick(function () { _this.updateVisibleItems(true); _this.$_ready = true; }); }, methods: { updateVisibleItems: function updateVisibleItems() { var _this2 = this; var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (!this.$_updateDirty) { this.$_updateDirty = true; this.$nextTick(function () { _this2.$_updateDirty = false; var l = _this2.items.length; var scroll = _this2.getScroll(); var items = _this2.items; var itemHeight = _this2.itemHeight; var containerHeight = void 0, offsetTop = void 0; if (scroll) { var startIndex = -1; var endIndex = -1; var buffer = parseInt(_this2.buffer); var poolSize = parseInt(_this2.poolSize); var scrollTop = ~~(scroll.top / poolSize) * poolSize - buffer; var scrollBottom = Math.ceil(scroll.bottom / poolSize) * poolSize + buffer; if (!force && (scrollTop === _this2.$_oldScrollTop && scrollBottom === _this2.$_oldScrollBottom || _this2.$_skip)) { _this2.$_skip = false; return; } else { _this2.$_oldScrollTop = scrollTop; _this2.$_oldScrollBottom = scrollBottom; } // Variable height mode if (itemHeight === null) { var heights = _this2.heights; var h = void 0; var a = 0; var b = l - 1; var i = ~~(l / 2); var oldI = void 0; // Searching for startIndex do { oldI = i; h = heights[i].accumulator; if (h < scrollTop) { a = i; } else if (i < l - 1 && heights[i + 1].accumulator > scrollTop) { b = i; } i = ~~((a + b) / 2); } while (i !== oldI); i < 0 && (i = 0); startIndex = i; // For containers style offsetTop = i > 0 ? heights[i - 1].accumulator : 0; containerHeight = heights[l - 1].accumulator; // Searching for endIndex for (endIndex = i; endIndex < l && heights[endIndex].accumulator < scrollBottom; endIndex++) {} if (endIndex === -1) { endIndex = items.length - 1; } else { endIndex++; // Bounds endIndex > l && (endIndex = l); } } else { // Fixed height mode startIndex = ~~(scrollTop / itemHeight); endIndex = Math.ceil(scrollBottom / itemHeight); // Bounds startIndex < 0 && (startIndex = 0); endIndex > l && (endIndex = l); offsetTop = startIndex * itemHeight; containerHeight = l * itemHeight; } if (force || _this2.$_startIndex !== startIndex || _this2.$_endIndex !== endIndex || _this2.$_offsetTop !== offsetTop || _this2.$_height !== containerHeight || _this2.$_length !== l) { _this2.keysEnabled = !(startIndex > _this2.$_endIndex || endIndex < _this2.$_startIndex); _this2.itemContainerStyle = { height: containerHeight + 'px' }; _this2.itemsStyle = { marginTop: offsetTop + 'px' }; if (_this2.delayPreviousItems) { // Add next items _this2.visibleItems = items.slice(_this2.$_startIndex, endIndex); // Remove previous items _this2.$nextTick(function () { _this2.visibleItems = items.slice(startIndex, endIndex); }); } else { _this2.visibleItems = items.slice(startIndex, endIndex); } _this2.emitUpdate && _this2.$emit('update', startIndex, endIndex); _this2.$_startIndex = startIndex; _this2.$_endIndex = endIndex; _this2.$_length = l; _this2.$_offsetTop = offsetTop; _this2.$_height = containerHeight; } } }); } }, setDirty: function setDirty() { this.$_oldScrollTop = null; this.$_oldScrollBottom = null; }, handleScroll: function handleScroll() { var _this3 = this; if (!this.$_scrollDirty) { this.$_scrollDirty = true; requestAnimationFrame(function () { _this3.$_scrollDirty = false; _this3.updateVisibleItems(); }); } }, handleResize: function handleResize() { this.$emit('resize'); this.$_ready && this.updateVisibleItems(); }, handleVisibilityChange: function handleVisibilityChange(isVisible, entry) { var _this4 = this; if (this.$_ready && (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0)) { this.$emit('visible'); this.$nextTick(function () { _this4.updateVisibleItems(); }); } } } }; var uid = 0; var RecycleList = { render: function render() { var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('div', { directives: [{ name: "observe-visibility", rawName: "v-observe-visibility", value: _vm.handleVisibilityChange, expression: "handleVisibilityChange" }], staticClass: "recycle-list", class: _vm.cssClass, on: { "&scroll": function scroll($event) { _vm.handleScroll($event); } } }, [_c('div', { ref: "wrapper", staticClass: "item-wrapper", style: { height: _vm.totalHeight + 'px' } }, _vm._l(_vm.pool, function (view) { return _c('div', { key: view.nr.id, staticClass: "item-view", style: { transform: 'translateY(' + view.top + 'px)' } }, [_vm._t("default", null, { item: view.item, index: view.nr.index, active: view.nr.used })], 2); })), _vm._v(" "), _vm._t("after-container"), _vm._v(" "), _c('resize-observer', { on: { "notify": _vm.handleResize } })], 2); }, staticRenderFns: [], _scopeId: 'data-v-2277f571', name: 'RecycleList', mixins: [Scroller], props: { itemHeight: { type: Number, default: null }, keyField: { type: String, default: null } }, data: function data() { return { pool: [], totalHeight: 0 }; }, watch: { items: { handler: function handler() { this.updateVisibleItems({ checkItem: true }); } }, pageMode: function pageMode() { this.applyPageMode(); this.updateVisibleItems({ checkItem: false }); }, heights: { handler: function handler() { this.updateVisibleItems({ checkItem: false }); }, deep: true } }, created: function created() { this.$_ready = false; this.$_startIndex = 0; this.$_endIndex = 0; this.$_views = new Map(); this.$_unusedViews = new Map(); this.$_scrollDirty = false; // TODO prerender }, mounted: function mounted() { var _this = this; this.applyPageMode(); this.$nextTick(function () { _this.updateVisibleItems({ checkItem: true }); _this.$_ready = true; }); }, methods: { addView: function addView(pool, index, item, key, type) { var view = { item: item, top: 0 }; var nonReactive = { id: uid++, index: index, used: true, key: key, type: type }; Object.defineProperty(view, 'nr', { configurable: false, value: nonReactive }); pool.push(view); return view; }, unuseView: function unuseView(view) { var fake = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var unusedViews = this.$_unusedViews; var type = view.nr.type; var unusedPool = unusedViews.get(type); if (!unusedPool) { unusedPool = []; unusedViews.set(type, unusedPool); } unusedPool.push(view); if (!fake) { view.nr.used = false; view.top = -9999; this.$_views.delete(view.nr.key); } }, handleResize: function handleResize() { this.$emit('resize'); this.$_ready && this.updateVisibleItems({ checkItem: false }); }, handleScroll: function handleScroll(event) { var _this2 = this; if (!this.$_scrollDirty) { this.$_scrollDirty = true; requestAnimationFrame(function () { _this2.$_scrollDirty = false; var _updateVisibleItems = _this2.updateVisibleItems({ checkItem: false }), continuous = _updateVisibleItems.continuous; // It seems sometimes chrome doesn't fire scroll event :/ // When non continous scrolling is ending, we force a refresh if (!continuous) { clearTimeout(_this2.$_refreshTimout); _this2.$_refreshTimout = setTimeout(_this2.handleScroll, 100); } }); } }, handleVisibilityChange: function handleVisibilityChange(isVisible, entry) { var _this3 = this; if (this.$_ready && (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0)) { this.$emit('visible'); requestAnimationFrame(function () { _this3.updateVisibleItems({ checkItem: false }); }); } }, updateVisibleItems: function updateVisibleItems(_ref) { var checkItem = _ref.checkItem; var scroll = this.getScroll(); var buffer = parseInt(this.buffer); scroll.top -= buffer; scroll.bottom += buffer; var itemHeight = this.itemHeight; var typeField = this.typeField; var keyField = this.keyField; var items = this.items; var count = items.length; var heights = this.heights; var views = this.$_views; var unusedViews = this.$_unusedViews; var pool = this.pool; var startIndex = void 0, endIndex = void 0; var totalHeight = void 0; if (!count) { startIndex = endIndex = totalHeight = 0; } else { // Variable height mode if (itemHeight === null) { var h = void 0; var a = 0; var b = count - 1; var i = ~~(count / 2); var oldI = void 0; // Searching for startIndex do { oldI = i; h = heights[i].accumulator; if (h < scroll.top) { a = i; } else if (i < count - 1 && heights[i + 1].accumulator > scroll.top) { b = i; } i = ~~((a + b) / 2); } while (i !== oldI); i < 0 && (i = 0); startIndex = i; // For container style totalHeight = heights[count - 1].accumulator; // Searching for endIndex for (endIndex = i; endIndex < count && heights[endIndex].accumulator < scroll.bottom; endIndex++) {} if (endIndex === -1) { endIndex = items.length - 1; } else { endIndex++; // Bounds endIndex > count && (endIndex = count); } } else { // Fixed height mode startIndex = ~~(scroll.top / itemHeight); endIndex = Math.ceil(scroll.bottom / itemHeight); // Bounds startIndex < 0 && (startIndex = 0); endIndex > count && (endIndex = count); totalHeight = count * itemHeight; } } this.totalHeight = totalHeight; var view = void 0; var continuous = startIndex <= this.$_endIndex && endIndex >= this.$_startIndex; var unusedIndex = void 0; if (this.$_continuous !== continuous) { if (continuous) { views.clear(); unusedViews.clear(); for (var _i = 0, l = pool.length; _i < l; _i++) { view = pool[_i]; this.unuseView(view); } } this.$_continuous = continuous; } else if (continuous) { for (var _i2 = 0, _l = pool.length; _i2 < _l; _i2++) { view = pool[_i2]; if (view.nr.used) { // Update view item index if (checkItem) view.nr.index = items.findIndex(function (item) { return keyField ? item[keyField] == view.item[keyField] : item === view.item; }); // Check if index is still in visible range if (view.nr.index === -1 || view.nr.index < startIndex || view.nr.index > endIndex) { this.unuseView(view); } } } } if (!continuous) { unusedIndex = new Map(); } var item = void 0, type = void 0, unusedPool = void 0; var v = void 0; for (var _i3 = startIndex; _i3 < endIndex; _i3++) { item = items[_i3]; var key = keyField ? item[keyField] : item; view = views.get(key); if (!itemHeight && !heights[_i3].height) { if (view) this.unuseView(view); continue; } // No view assigned to item if (!view) { type = item[typeField]; if (continuous) { unusedPool = unusedViews.get(type); // Reuse existing view if (unusedPool && unusedPool.length) { view = unusedPool.pop(); view.item = item; view.nr.used = true; view.nr.index = _i3; view.nr.key = key; view.nr.type = type; } else { view = this.addView(pool, _i3, item, key, type); } } else { unusedPool = unusedViews.get(type); v = unusedIndex.get(type) || 0; // Use existing view // We don't care if they are already used // because we are not in continous scrolling if (unusedPool && v < unusedPool.length) { view = unusedPool[v]; view.item = item; view.nr.used = true; view.nr.index = _i3; view.nr.key = key; view.nr.type = type; unusedIndex.set(type, v + 1); } else { view = this.addView(pool, _i3, item, key, type); this.unuseView(view, true); } v++; } views.set(key, view); } else { view.nr.used = true; } // Update position if (itemHeight === null) { view.top = heights[_i3 - 1].accumulator; } else { view.top = _i3 * itemHeight; } } this.$_startIndex = startIndex; this.$_endIndex = endIndex; this.emitUpdate && this.$emit('update', startIndex, endIndex); return { continuous: continuous }; } } }; function registerComponents(Vue, prefix) { Vue.component(prefix + 'virtual-scroller', VirtualScroller); Vue.component(prefix + 'recycle-list', RecycleList); } var plugin$4 = { // eslint-disable-next-line no-undef version: "0.11.7", install: function install(Vue, options) { var finalOptions = Object.assign({}, { installComponents: true, componentsPrefix: '' }, options); if (finalOptions.installComponents) { registerComponents(Vue, finalOptions.componentsPrefix); } } }; // Auto-install var GlobalVue$2 = null; if (typeof window !== 'undefined') { GlobalVue$2 = window.Vue; } else if (typeof global !== 'undefined') { GlobalVue$2 = global.Vue; } if (GlobalVue$2) { GlobalVue$2.use(plugin$4); } export default plugin$4; export { VirtualScroller, RecycleList };