quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
751 lines (616 loc) • 22 kB
JavaScript
import { h, ref, computed, watch, onActivated, onDeactivated, onBeforeMount, onBeforeUnmount, nextTick, getCurrentInstance } from 'vue'
import debounce from '../../utils/debounce.js'
import { noop } from '../../utils/event.js'
import { rtlHasScrollBug } from '../../utils/private/rtl.js'
const aggBucketSize = 1000
const scrollToEdges = [
'start',
'center',
'end',
'start-force',
'center-force',
'end-force'
]
const filterProto = Array.prototype.filter
const setOverflowAnchor = __QUASAR_SSR__ || window.getComputedStyle(document.body).overflowAnchor === void 0
? noop
: function (contentEl, index) {
if (contentEl === null) {
return
}
if (contentEl._qOverflowAnimationFrame !== void 0) {
cancelAnimationFrame(contentEl._qOverflowAnimationFrame)
}
contentEl._qOverflowAnimationFrame = requestAnimationFrame(() => {
if (contentEl === null) {
return
}
contentEl._qOverflowAnimationFrame = void 0
const children = contentEl.children || []
filterProto
.call(children, el => el.dataset && el.dataset.qVsAnchor !== void 0)
.forEach(el => {
delete el.dataset.qVsAnchor
})
const el = children[ index ]
if (el && el.dataset) {
el.dataset.qVsAnchor = ''
}
})
}
function sumFn (acc, h) {
return acc + h
}
function getScrollDetails (
parent,
child,
beforeRef,
afterRef,
horizontal,
rtl,
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 += document.documentElement.clientWidth
}
else {
details.scrollStart = parentCalc.scrollLeft
details.scrollViewSize += parentCalc.clientWidth
}
details.scrollMaxSize = parentCalc.scrollWidth
if (rtl === true) {
details.scrollStart = (rtlHasScrollBug === true ? details.scrollMaxSize - details.scrollViewSize : 0) - details.scrollStart
}
}
else {
if (parent === window) {
details.scrollStart = window.pageYOffset || window.scrollY || document.body.scrollTop || 0
details.scrollViewSize += document.documentElement.clientHeight
}
else {
details.scrollStart = parentCalc.scrollTop
details.scrollViewSize += parentCalc.clientHeight
}
details.scrollMaxSize = parentCalc.scrollHeight
}
if (beforeRef !== null) {
for (let el = beforeRef.previousElementSibling; el !== null; el = el.previousElementSibling) {
if (el.classList.contains('q-virtual-scroll--skip') === false) {
details.offsetStart += el[ propElSize ]
}
}
}
if (afterRef !== null) {
for (let el = afterRef.nextElementSibling; el !== null; el = el.nextElementSibling) {
if (el.classList.contains('q-virtual-scroll--skip') === false) {
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, rtl) {
if (scroll === 'end') {
scroll = (parent === window ? document.body : parent)[
horizontal === true ? 'scrollWidth' : 'scrollHeight'
]
}
if (parent === window) {
if (horizontal === true) {
if (rtl === true) {
scroll = (rtlHasScrollBug === true ? document.body.scrollWidth - document.documentElement.clientWidth : 0) - scroll
}
window.scrollTo(scroll, window.pageYOffset || window.scrollY || document.body.scrollTop || 0)
}
else {
window.scrollTo(window.pageXOffset || window.scrollX || document.body.scrollLeft || 0, scroll)
}
}
else if (horizontal === true) {
if (rtl === true) {
scroll = (rtlHasScrollBug === true ? parent.scrollWidth - parent.offsetWidth : 0) - scroll
}
parent.scrollLeft = scroll
}
else {
parent.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, String ],
default: null
},
virtualScrollSliceRatioBefore: {
type: [ Number, String ],
default: 1
},
virtualScrollSliceRatioAfter: {
type: [ Number, String ],
default: 1
},
virtualScrollItemSize: {
type: [ Number, String ],
default: 24
},
virtualScrollStickySizeStart: {
type: [ Number, String ],
default: 0
},
virtualScrollStickySizeEnd: {
type: [ Number, String ],
default: 0
},
tableColspan: [ Number, String ]
}
export const commonVirtPropsList = Object.keys(commonVirtScrollProps)
export const useVirtualScrollProps = {
virtualScrollHorizontal: Boolean,
onVirtualScroll: Function,
...commonVirtScrollProps
}
export function useVirtualScroll ({
virtualScrollLength, getVirtualScrollTarget, getVirtualScrollEl,
virtualScrollItemSizeComputed // optional
}) {
const vm = getCurrentInstance()
const { props, emit, proxy } = vm
const { $q } = proxy
let prevScrollStart, prevToIndex, localScrollViewSize, virtualScrollSizesAgg = [], virtualScrollSizes
const virtualScrollPaddingBefore = ref(0)
const virtualScrollPaddingAfter = ref(0)
const virtualScrollSliceSizeComputed = ref({})
const beforeRef = ref(null)
const afterRef = ref(null)
const contentRef = ref(null)
const virtualScrollSliceRange = ref({ from: 0, to: 0 })
const colspanAttr = computed(() => (props.tableColspan !== void 0 ? props.tableColspan : 100))
if (virtualScrollItemSizeComputed === void 0) {
virtualScrollItemSizeComputed = computed(() => props.virtualScrollItemSize)
}
const needsReset = computed(() => virtualScrollItemSizeComputed.value + ';' + props.virtualScrollHorizontal)
const needsSliceRecalc = computed(() =>
needsReset.value + ';' + props.virtualScrollSliceRatioBefore + ';' + props.virtualScrollSliceRatioAfter
)
watch(needsSliceRecalc, () => { setVirtualScrollSize() })
watch(needsReset, reset)
function reset () {
localResetVirtualScroll(prevToIndex, true)
}
function refresh (toIndex) {
localResetVirtualScroll(toIndex === void 0 ? prevToIndex : toIndex)
}
function scrollTo (toIndex, edge) {
const scrollEl = getVirtualScrollTarget()
if (scrollEl === void 0 || scrollEl === null || scrollEl.nodeType === 8) {
return
}
const scrollDetails = getScrollDetails(
scrollEl,
getVirtualScrollEl(),
beforeRef.value,
afterRef.value,
props.virtualScrollHorizontal,
$q.lang.rtl,
props.virtualScrollStickySizeStart,
props.virtualScrollStickySizeEnd
)
localScrollViewSize !== scrollDetails.scrollViewSize && setVirtualScrollSize(scrollDetails.scrollViewSize)
setVirtualScrollSliceRange(
scrollEl,
scrollDetails,
Math.min(virtualScrollLength.value - 1, Math.max(0, parseInt(toIndex, 10) || 0)),
0,
scrollToEdges.indexOf(edge) > -1 ? edge : (prevToIndex > -1 && toIndex > prevToIndex ? 'end' : 'start')
)
}
function localOnVirtualScrollEvt () {
const scrollEl = getVirtualScrollTarget()
if (scrollEl === void 0 || scrollEl === null || scrollEl.nodeType === 8) {
return
}
const
scrollDetails = getScrollDetails(
scrollEl,
getVirtualScrollEl(),
beforeRef.value,
afterRef.value,
props.virtualScrollHorizontal,
$q.lang.rtl,
props.virtualScrollStickySizeStart,
props.virtualScrollStickySizeEnd
),
listLastIndex = virtualScrollLength.value - 1,
listEndOffset = scrollDetails.scrollMaxSize - scrollDetails.offsetStart - scrollDetails.offsetEnd - virtualScrollPaddingAfter.value
if (prevScrollStart === scrollDetails.scrollStart) {
return
}
if (scrollDetails.scrollMaxSize <= 0) {
setVirtualScrollSliceRange(scrollEl, scrollDetails, 0, 0)
return
}
localScrollViewSize !== scrollDetails.scrollViewSize && setVirtualScrollSize(scrollDetails.scrollViewSize)
updateVirtualScrollSizes(virtualScrollSliceRange.value.from)
const scrollMaxStart = Math.floor(scrollDetails.scrollMaxSize
- Math.max(scrollDetails.scrollViewSize, scrollDetails.offsetEnd)
- Math.min(virtualScrollSizes[ listLastIndex ], scrollDetails.scrollViewSize / 2))
if (scrollMaxStart > 0 && Math.ceil(scrollDetails.scrollStart) >= scrollMaxStart) {
setVirtualScrollSliceRange(
scrollEl,
scrollDetails,
listLastIndex,
scrollDetails.scrollMaxSize - scrollDetails.offsetEnd - virtualScrollSizesAgg.reduce(sumFn, 0)
)
return
}
let
toIndex = 0,
listOffset = scrollDetails.scrollStart - scrollDetails.offsetStart,
offset = listOffset
if (listOffset <= listEndOffset && listOffset + scrollDetails.scrollViewSize >= virtualScrollPaddingBefore.value) {
listOffset -= virtualScrollPaddingBefore.value
toIndex = virtualScrollSliceRange.value.from
offset = listOffset
}
else {
for (let j = 0; listOffset >= virtualScrollSizesAgg[ j ] && toIndex < listLastIndex; j++) {
listOffset -= virtualScrollSizesAgg[ j ]
toIndex += aggBucketSize
}
}
while (listOffset > 0 && toIndex < listLastIndex) {
listOffset -= virtualScrollSizes[ toIndex ]
if (listOffset > -scrollDetails.scrollViewSize) {
toIndex++
offset = listOffset
}
else {
offset = virtualScrollSizes[ toIndex ] + listOffset
}
}
setVirtualScrollSliceRange(
scrollEl,
scrollDetails,
toIndex,
offset
)
}
function setVirtualScrollSliceRange (scrollEl, scrollDetails, toIndex, offset, align) {
const alignForce = typeof align === 'string' && align.indexOf('-force') > -1
const alignEnd = alignForce === true ? align.replace('-force', '') : align
const alignRange = alignEnd !== void 0 ? alignEnd : 'start'
let
from = Math.max(0, toIndex - virtualScrollSliceSizeComputed.value[ alignRange ]),
to = from + virtualScrollSliceSizeComputed.value.total
if (to > virtualScrollLength.value) {
to = virtualScrollLength.value
from = Math.max(0, to - virtualScrollSliceSizeComputed.value.total)
}
prevScrollStart = scrollDetails.scrollStart
const rangeChanged = from !== virtualScrollSliceRange.value.from || to !== virtualScrollSliceRange.value.to
if (rangeChanged === false && alignEnd === void 0) {
emitScroll(toIndex)
return
}
const { activeElement } = document
const contentEl = contentRef.value
if (
rangeChanged === true
&& contentEl !== null
&& contentEl !== activeElement
&& contentEl.contains(activeElement) === true
) {
contentEl.addEventListener('focusout', onBlurRefocusFn)
setTimeout(() => {
contentEl !== null && contentEl.removeEventListener('focusout', onBlurRefocusFn)
})
}
setOverflowAnchor(contentEl, toIndex - from)
const sizeBefore = alignEnd !== void 0 ? virtualScrollSizes.slice(from, toIndex).reduce(sumFn, 0) : 0
if (rangeChanged === true) {
// vue key matching algorithm works only if
// the array of VNodes changes on only one of the ends
// so we first change one end and then the other
const tempTo = to >= virtualScrollSliceRange.value.from && from <= virtualScrollSliceRange.value.to
? virtualScrollSliceRange.value.to
: to
virtualScrollSliceRange.value = { from, to: tempTo }
virtualScrollPaddingBefore.value = sumSize(virtualScrollSizesAgg, virtualScrollSizes, 0, from)
virtualScrollPaddingAfter.value = sumSize(virtualScrollSizesAgg, virtualScrollSizes, to, virtualScrollLength.value)
requestAnimationFrame(() => {
if (virtualScrollSliceRange.value.to !== to && prevScrollStart === scrollDetails.scrollStart) {
virtualScrollSliceRange.value = { from: virtualScrollSliceRange.value.from, to }
virtualScrollPaddingAfter.value = sumSize(virtualScrollSizesAgg, virtualScrollSizes, to, virtualScrollLength.value)
}
})
}
requestAnimationFrame(() => {
// if the scroll was changed give up
// (another call to setVirtualScrollSliceRange before animation frame)
if (prevScrollStart !== scrollDetails.scrollStart) {
return
}
if (rangeChanged === true) {
updateVirtualScrollSizes(from)
}
const
sizeAfter = virtualScrollSizes.slice(from, toIndex).reduce(sumFn, 0),
posStart = sizeAfter + scrollDetails.offsetStart + virtualScrollPaddingBefore.value,
posEnd = posStart + virtualScrollSizes[ toIndex ]
let scrollPosition = posStart + offset
if (alignEnd !== void 0) {
const sizeDiff = sizeAfter - sizeBefore
const scrollStart = scrollDetails.scrollStart + sizeDiff
scrollPosition = alignForce !== true && scrollStart < posStart && posEnd < scrollStart + scrollDetails.scrollViewSize
? scrollStart
: (
alignEnd === 'end'
? posEnd - scrollDetails.scrollViewSize
: posStart - (alignEnd === 'start' ? 0 : Math.round((scrollDetails.scrollViewSize - virtualScrollSizes[ toIndex ]) / 2))
)
}
prevScrollStart = scrollPosition
setScroll(
scrollEl,
scrollPosition,
props.virtualScrollHorizontal,
$q.lang.rtl
)
emitScroll(toIndex)
})
}
function updateVirtualScrollSizes (from) {
const contentEl = contentRef.value
if (contentEl) {
const
children = filterProto.call(
contentEl.children,
el => el.classList && el.classList.contains('q-virtual-scroll--skip') === false
),
childrenLength = children.length,
sizeFn = props.virtualScrollHorizontal === true
? el => el.getBoundingClientRect().width
: el => el.offsetHeight
let
index = from,
size, diff
for (let i = 0; i < childrenLength;) {
size = sizeFn(children[ i ])
i++
while (i < childrenLength && children[ i ].classList.contains('q-virtual-scroll--with-prev') === true) {
size += sizeFn(children[ i ])
i++
}
diff = size - virtualScrollSizes[ index ]
if (diff !== 0) {
virtualScrollSizes[ index ] += diff
virtualScrollSizesAgg[ Math.floor(index / aggBucketSize) ] += diff
}
index++
}
}
}
function onBlurRefocusFn () {
contentRef.value !== null && contentRef.value !== void 0 && contentRef.value.focus()
}
function localResetVirtualScroll (toIndex, fullReset) {
const defaultSize = 1 * virtualScrollItemSizeComputed.value
if (fullReset === true || Array.isArray(virtualScrollSizes) === false) {
virtualScrollSizes = []
}
const oldVirtualScrollSizesLength = virtualScrollSizes.length
virtualScrollSizes.length = virtualScrollLength.value
for (let i = virtualScrollLength.value - 1; i >= oldVirtualScrollSizesLength; i--) {
virtualScrollSizes[ i ] = defaultSize
}
const jMax = Math.floor((virtualScrollLength.value - 1) / aggBucketSize)
virtualScrollSizesAgg = []
for (let j = 0; j <= jMax; j++) {
let size = 0
const iMax = Math.min((j + 1) * aggBucketSize, virtualScrollLength.value)
for (let i = j * aggBucketSize; i < iMax; i++) {
size += virtualScrollSizes[ i ]
}
virtualScrollSizesAgg.push(size)
}
prevToIndex = -1
prevScrollStart = void 0
virtualScrollPaddingBefore.value = sumSize(virtualScrollSizesAgg, virtualScrollSizes, 0, virtualScrollSliceRange.value.from)
virtualScrollPaddingAfter.value = sumSize(virtualScrollSizesAgg, virtualScrollSizes, virtualScrollSliceRange.value.to, virtualScrollLength.value)
if (toIndex >= 0) {
updateVirtualScrollSizes(virtualScrollSliceRange.value.from)
nextTick(() => { scrollTo(toIndex) })
}
else {
onVirtualScrollEvt()
}
}
function setVirtualScrollSize (scrollViewSize) {
if (scrollViewSize === void 0 && typeof window !== 'undefined') {
const scrollEl = getVirtualScrollTarget()
if (scrollEl !== void 0 && scrollEl !== null && scrollEl.nodeType !== 8) {
scrollViewSize = getScrollDetails(
scrollEl,
getVirtualScrollEl(),
beforeRef.value,
afterRef.value,
props.virtualScrollHorizontal,
$q.lang.rtl,
props.virtualScrollStickySizeStart,
props.virtualScrollStickySizeEnd
).scrollViewSize
}
}
localScrollViewSize = scrollViewSize
const virtualScrollSliceRatioBefore = parseFloat(props.virtualScrollSliceRatioBefore) || 0
const virtualScrollSliceRatioAfter = parseFloat(props.virtualScrollSliceRatioAfter) || 0
const multiplier = 1 + virtualScrollSliceRatioBefore + virtualScrollSliceRatioAfter
const view = scrollViewSize === void 0 || scrollViewSize <= 0
? 1
: Math.ceil(scrollViewSize / virtualScrollItemSizeComputed.value)
const baseSize = Math.max(
1,
view,
Math.ceil((props.virtualScrollSliceSize > 0 ? props.virtualScrollSliceSize : 10) / multiplier)
)
virtualScrollSliceSizeComputed.value = {
total: Math.ceil(baseSize * multiplier),
start: Math.ceil(baseSize * virtualScrollSliceRatioBefore),
center: Math.ceil(baseSize * (0.5 + virtualScrollSliceRatioBefore)),
end: Math.ceil(baseSize * (1 + virtualScrollSliceRatioBefore)),
view
}
}
function padVirtualScroll (tag, content) {
const paddingSize = props.virtualScrollHorizontal === true ? 'width' : 'height'
const style = {
[ '--q-virtual-scroll-item-' + paddingSize ]: virtualScrollItemSizeComputed.value + 'px'
}
return [
tag === 'tbody'
? h(tag, {
class: 'q-virtual-scroll__padding',
key: 'before',
ref: beforeRef
}, [
h('tr', [
h('td', {
style: { [ paddingSize ]: `${ virtualScrollPaddingBefore.value }px`, ...style },
colspan: colspanAttr.value
})
])
])
: h(tag, {
class: 'q-virtual-scroll__padding',
key: 'before',
ref: beforeRef,
style: { [ paddingSize ]: `${ virtualScrollPaddingBefore.value }px`, ...style }
}),
h(tag, {
class: 'q-virtual-scroll__content',
key: 'content',
ref: contentRef,
tabindex: -1
}, content.flat()),
tag === 'tbody'
? h(tag, {
class: 'q-virtual-scroll__padding',
key: 'after',
ref: afterRef
}, [
h('tr', [
h('td', {
style: { [ paddingSize ]: `${ virtualScrollPaddingAfter.value }px`, ...style },
colspan: colspanAttr.value
})
])
])
: h(tag, {
class: 'q-virtual-scroll__padding',
key: 'after',
ref: afterRef,
style: { [ paddingSize ]: `${ virtualScrollPaddingAfter.value }px`, ...style }
})
]
}
function emitScroll (index) {
if (prevToIndex !== index) {
props.onVirtualScroll !== void 0 && emit('virtualScroll', {
index,
from: virtualScrollSliceRange.value.from,
to: virtualScrollSliceRange.value.to - 1,
direction: index < prevToIndex ? 'decrease' : 'increase',
ref: proxy
})
prevToIndex = index
}
}
setVirtualScrollSize()
const onVirtualScrollEvt = debounce(
localOnVirtualScrollEvt,
$q.platform.is.ios === true ? 120 : 35
)
onBeforeMount(() => {
setVirtualScrollSize()
})
let shouldActivate = false
onDeactivated(() => {
shouldActivate = true
})
onActivated(() => {
if (shouldActivate !== true) { return }
const scrollEl = getVirtualScrollTarget()
if (prevScrollStart !== void 0 && scrollEl !== void 0 && scrollEl !== null && scrollEl.nodeType !== 8) {
setScroll(
scrollEl,
prevScrollStart,
props.virtualScrollHorizontal,
$q.lang.rtl
)
}
else {
scrollTo(prevToIndex)
}
})
__QUASAR_SSR__ || onBeforeUnmount(() => {
onVirtualScrollEvt.cancel()
})
// expose public methods
Object.assign(proxy, { scrollTo, reset, refresh })
return {
virtualScrollSliceRange,
virtualScrollSliceSizeComputed,
setVirtualScrollSize,
onVirtualScrollEvt,
localResetVirtualScroll,
padVirtualScroll,
scrollTo,
reset,
refresh
}
}