UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

460 lines (387 loc) 13.4 kB
import debounce from '../utils/debounce.js' import frameDebounce from '../utils/frame-debounce.js' const aggBucketSize = 1000 function sumFn (acc, h) { return acc + h } function getScrollDetails ( parent, child, beforeRef, afterRef, horizontal, stickyStart, stickyEnd ) { const parentCalc = parent === window ? document.scrollingElement || document.documentElement : parent, propElSize = horizontal === true ? 'offsetWidth' : 'offsetHeight', details = { scrollStart: 0, scrollViewSize: -stickyStart - stickyEnd, scrollMaxSize: 0, offsetStart: -stickyStart, offsetEnd: -stickyEnd } if (horizontal === true) { if (parent === window) { details.scrollStart = window.pageXOffset || window.scrollX || document.body.scrollLeft || 0 details.scrollViewSize += window.innerWidth } else { details.scrollStart = parentCalc.scrollLeft details.scrollViewSize += parentCalc.clientWidth } details.scrollMaxSize = parentCalc.scrollWidth } else { if (parent === window) { details.scrollStart = window.pageYOffset || window.scrollY || document.body.scrollTop || 0 details.scrollViewSize += window.innerHeight } else { details.scrollStart = parentCalc.scrollTop details.scrollViewSize += parentCalc.clientHeight } details.scrollMaxSize = parentCalc.scrollHeight } if (beforeRef !== void 0) { for (let el = beforeRef.previousElementSibling; el !== null; el = el.previousElementSibling) { details.offsetStart += el[propElSize] } } if (afterRef !== void 0) { for (let el = afterRef.nextElementSibling; el !== null; el = el.nextElementSibling) { details.offsetEnd += el[propElSize] } } if (child !== parent) { const parentRect = parentCalc.getBoundingClientRect(), childRect = child.getBoundingClientRect() if (horizontal === true) { details.offsetStart += childRect.left - parentRect.left details.offsetEnd -= childRect.width } else { details.offsetStart += childRect.top - parentRect.top details.offsetEnd -= childRect.height } if (parent !== window) { details.offsetStart += details.scrollStart } details.offsetEnd += details.scrollMaxSize - details.offsetStart } return details } function setScroll (parent, scroll, horizontal) { if (parent === window) { if (horizontal === true) { window.scrollTo(scroll, window.pageYOffset || window.scrollY || document.body.scrollTop || 0) } else { window.scrollTo(window.pageXOffset || window.scrollX || document.body.scrollLeft || 0, scroll) } } else { parent[horizontal === true ? 'scrollLeft' : 'scrollTop'] = scroll } } function sumSize (sizeAgg, size, from, to) { if (from >= to) { return 0 } const lastTo = size.length, fromAgg = Math.floor(from / aggBucketSize), toAgg = Math.floor((to - 1) / aggBucketSize) + 1 let total = sizeAgg.slice(fromAgg, toAgg).reduce(sumFn, 0) if (from % aggBucketSize !== 0) { total -= size.slice(fromAgg * aggBucketSize, from).reduce(sumFn, 0) } if (to % aggBucketSize !== 0 && to !== lastTo) { total -= size.slice(to, toAgg * aggBucketSize).reduce(sumFn, 0) } return total } const commonVirtScrollProps = { virtualScrollSliceSize: { type: Number, default: 30 }, virtualScrollItemSize: { type: Number, default: 24 }, virtualScrollStickySizeStart: { type: Number, default: 0 }, virtualScrollStickySizeEnd: { type: Number, default: 0 } } export const commonVirtPropsList = Object.keys(commonVirtScrollProps) export default { props: { virtualScrollHorizontal: Boolean, ...commonVirtScrollProps }, data () { return { virtualScrollSliceRange: { from: 0, to: 0 } } }, watch: { virtualScrollHorizontal () { this.__setVirtualScrollSize() }, needsReset () { this.reset() } }, computed: { needsReset () { return ['virtualScrollItemSize', 'virtualScrollHorizontal'] .map(p => this[p]).join(';') } }, methods: { reset () { this.__resetVirtualScroll(this.prevToIndex, true) }, scrollTo (toIndex) { const scrollEl = this.__getVirtualScrollTarget() if (scrollEl === void 0 || scrollEl === null || scrollEl.nodeType === 8) { return } this.__setVirtualScrollSliceRange( scrollEl, getScrollDetails( scrollEl, this.__getVirtualScrollEl(), this.$refs.before, this.$refs.after, this.virtualScrollHorizontal, this.virtualScrollStickySizeStart, this.virtualScrollStickySizeEnd ), Math.min(this.virtualScrollLength - 1, Math.max(0, parseInt(toIndex, 10) || 0)), 0, this.prevToIndex > -1 && toIndex > this.prevToIndex ? 'end' : 'start' ) }, __onVirtualScrollEvt () { const scrollEl = this.__getVirtualScrollTarget() if (scrollEl === void 0 || scrollEl === null || scrollEl.nodeType === 8) { return } const scrollDetails = getScrollDetails( scrollEl, this.__getVirtualScrollEl(), this.$refs.before, this.$refs.after, this.virtualScrollHorizontal, this.virtualScrollStickySizeStart, this.virtualScrollStickySizeEnd ), scrollMaxStart = scrollDetails.scrollMaxSize - Math.max(scrollDetails.scrollViewSize, scrollDetails.offsetEnd), listLastIndex = this.virtualScrollLength - 1 if (this.prevScrollStart === scrollDetails.scrollStart) { return } this.prevScrollStart = void 0 if (scrollMaxStart > 0 && scrollDetails.scrollStart >= scrollMaxStart) { this.__setVirtualScrollSliceRange( scrollEl, scrollDetails, this.virtualScrollLength - 1, scrollMaxStart - this.virtualScrollSizesAgg.reduce(sumFn, 0) ) return } let toIndex = 0, listOffset = scrollDetails.scrollStart - scrollDetails.offsetStart for (let j = 0; listOffset >= this.virtualScrollSizesAgg[j] && toIndex < listLastIndex; j++) { listOffset -= this.virtualScrollSizesAgg[j] toIndex += aggBucketSize } while (listOffset > 0 && toIndex < listLastIndex) { listOffset -= this.virtualScrollSizes[toIndex] if (listOffset > -scrollDetails.scrollViewSize) { toIndex++ } } this.__setVirtualScrollSliceRange( scrollEl, scrollDetails, toIndex, listOffset ) }, __setVirtualScrollSliceRange (scrollEl, scrollDetails, toIndex, offset, align) { let from = Math.max(0, Math.ceil(toIndex - (align === void 0 ? 3 : 2) * this.virtualScrollSliceSizeComputed / 6)), to = from + this.virtualScrollSliceSizeComputed if (to > this.virtualScrollLength) { to = this.virtualScrollLength from = Math.max(0, to - this.virtualScrollSliceSizeComputed) } this.__emitScroll(toIndex) const rangeChanged = from !== this.virtualScrollSliceRange.from || to !== this.virtualScrollSliceRange.to if (rangeChanged === false && align === void 0) { return } if (rangeChanged === true) { this.virtualScrollSliceRange = { from, to } this.virtualScrollPaddingBefore = sumSize(this.virtualScrollSizesAgg, this.virtualScrollSizes, 0, from) this.virtualScrollPaddingAfter = sumSize(this.virtualScrollSizesAgg, this.virtualScrollSizes, to, this.virtualScrollLength) } this.$nextTick(() => { if (rangeChanged === true) { const contentEl = this.$refs.content if (contentEl !== void 0) { const children = contentEl.children for (let i = children.length - 1; i >= 0; i--) { const index = from + i, diff = children[i][this.virtualScrollHorizontal === true ? 'offsetWidth' : 'offsetHeight'] - this.virtualScrollSizes[index] if (diff !== 0) { this.virtualScrollSizes[index] += diff this.virtualScrollSizesAgg[Math.floor(index / aggBucketSize)] += diff } } } } const posStart = this.virtualScrollSizes.slice(from, toIndex).reduce(sumFn, scrollDetails.offsetStart + this.virtualScrollPaddingBefore), posEnd = posStart + this.virtualScrollSizes[toIndex] let scrollPosition = posStart + offset if (align !== void 0) { scrollPosition = scrollDetails.scrollStart < posStart && posEnd < scrollDetails.scrollStart + scrollDetails.scrollViewSize ? scrollDetails.scrollStart : (align === 'end' ? posEnd - scrollDetails.scrollViewSize : posStart) } this.prevScrollStart = scrollPosition this.__setScroll( scrollEl, scrollPosition, this.virtualScrollHorizontal ) }) }, __resetVirtualScroll (toIndex, fullReset) { const defaultSize = this.virtualScrollItemSize if (fullReset === true || Array.isArray(this.virtualScrollSizes) === false) { this.virtualScrollSizes = [] } const oldVirtualScrollSizesLength = this.virtualScrollSizes.length this.virtualScrollSizes.length = this.virtualScrollLength for (let i = this.virtualScrollLength - 1; i >= oldVirtualScrollSizesLength; i--) { this.virtualScrollSizes[i] = defaultSize } const jMax = Math.floor((this.virtualScrollLength - 1) / aggBucketSize) this.virtualScrollSizesAgg = [] for (let j = 0; j <= jMax; j++) { let size = 0 const iMax = Math.min((j + 1) * aggBucketSize, this.virtualScrollLength) for (let i = j * aggBucketSize; i < iMax; i++) { size += this.virtualScrollSizes[i] } this.virtualScrollSizesAgg.push(size) } this.prevToIndex = -1 this.prevScrollStart = void 0 if (toIndex >= 0) { this.$nextTick(() => { this.scrollTo(toIndex) }) } else { this.virtualScrollPaddingBefore = sumSize(this.virtualScrollSizesAgg, this.virtualScrollSizes, 0, this.virtualScrollSliceRange.from) this.virtualScrollPaddingAfter = sumSize(this.virtualScrollSizesAgg, this.virtualScrollSizes, this.virtualScrollSliceRange.to, this.virtualScrollLength) this.__onVirtualScrollEvt() } }, __setVirtualScrollSize () { if (this.virtualScrollHorizontal === true) { this.virtualScrollSliceSizeComputed = typeof window === 'undefined' ? this.virtualScrollSliceSize : Math.max(this.virtualScrollSliceSize, Math.ceil(window.innerWidth / this.virtualScrollItemSize * 2)) } else { this.virtualScrollSliceSizeComputed = typeof window === 'undefined' ? this.virtualScrollSliceSize : Math.max(this.virtualScrollSliceSize, Math.ceil(window.innerHeight / this.virtualScrollItemSize * 2)) } }, __padVirtualScroll (h, tag, content) { const paddingSize = this.virtualScrollHorizontal === true ? 'width' : 'height' return [ tag === 'tbody' ? h(tag, { staticClass: 'q-virtual-scroll__padding', key: 'before', ref: 'before' }, [ h('tr', [ h('td', { style: { [paddingSize]: `${this.virtualScrollPaddingBefore}px` }, attrs: { colspan: '100%' } }) ]) ]) : h(tag, { staticClass: 'q-virtual-scroll__padding', key: 'before', ref: 'before', style: { [paddingSize]: `${this.virtualScrollPaddingBefore}px` } }), h(tag, { staticClass: 'q-virtual-scroll__content', key: 'content', ref: 'content' }, content), tag === 'tbody' ? h(tag, { staticClass: 'q-virtual-scroll__padding', key: 'after', ref: 'after' }, [ h('tr', [ h('td', { style: { [paddingSize]: `${this.virtualScrollPaddingAfter}px` }, attrs: { colspan: '100%' } }) ]) ]) : h(tag, { staticClass: 'q-virtual-scroll__padding', key: 'after', ref: 'after', style: { [paddingSize]: `${this.virtualScrollPaddingAfter}px` } }) ] }, __emitScroll (index) { if (this.prevToIndex !== index) { this.$listeners['virtual-scroll'] !== void 0 && this.$emit('virtual-scroll', { index, from: this.virtualScrollSliceRange.from, to: this.virtualScrollSliceRange.to - 1, direction: index < this.prevToIndex ? 'decrease' : 'increase' }) this.prevToIndex = index } } }, created () { this.__setVirtualScrollSize() }, beforeMount () { this.__onVirtualScrollEvt = debounce(this.__onVirtualScrollEvt, 70) this.__setScroll = frameDebounce(setScroll) this.__setVirtualScrollSize() } }