import { calcScrollWidth } from '@opentiny/utils'
export const computedBarSize = (props) => () => {
let barSize = +props.scrollbarSize
if (isNaN(barSize) || barSize < 0) {
barSize = calcScrollWidth()
}
return barSize
}
export const analyzeScrolling =
({ props, state }) =>
() => {
const width = +props.width
const height = +props.height
const scrollbarSize = state.scrollbarSize
const columnSizes = props.columnSizes
const rowSizes = props.rowSizes
const columnTotal = columnSizes.reduce((p, c) => p + c, 0)
const rowTotal = rowSizes.reduce((p, r) => p + r, 0)
const result = {
isScrollX: false,
isScrollY: false,
columnTotal,
rowTotal
}
if (columnTotal > width && rowTotal > height) {
result.isScrollX = true
result.isScrollY = true
} else if (columnTotal <= width && rowTotal <= height) {
result.isScrollX = false
result.isScrollY = false
} else if (columnTotal <= width && rowTotal > height) {
result.isScrollX = columnTotal + scrollbarSize > width
result.isScrollY = true
} else if (columnTotal > width && rowTotal <= height) {
result.isScrollX = true
result.isScrollY = rowTotal + scrollbarSize > height
}
return result
}
const preprocessVirtualScrollSlice = ({ items, itemSizes, fixedItems }) => {
const hasStart = fixedItems[0].length > 0
const hasEnd = fixedItems[1].length > 0
const startInfos = []
const infos = []
const endInfos = []
let startSize = 0
let endSize = 0
items.forEach((item, i) => {
const raw = item
const size = itemSizes[i]
const info = {
raw,
size,
start: 0,
end: 0,
index: i,
fixed: undefined,
offset: undefined,
startLast: undefined,
endFirst: undefined,
isLast: undefined
}
if (hasStart && fixedItems[0].includes(i)) {
startInfos.push(info)
startSize += size
info.fixed = 'start'
} else if (hasEnd && fixedItems[1].includes(i)) {
endInfos.push(info)
endSize += size
info.fixed = 'end'
} else {
infos.push(info)
}
})
let allInfos = [...infos]
if (hasStart || hasEnd) {
allInfos = [...startInfos, ...infos, ...endInfos]
if (hasStart && startInfos[startInfos.length - 1]) {
startInfos[startInfos.length - 1].startLast = true
}
if (hasEnd && endInfos[0]) {
endInfos[0].endFirst = true
}
}
if (allInfos[allInfos.length - 1]) {
allInfos[allInfos.length - 1].isLast = true
}
let reverseAllInfos = [...allInfos].reverse()
let total = 0
allInfos.forEach((info) => {
info.start = total
total += info.size
})
total = 0
reverseAllInfos.forEach((info) => {
info.end = total
total += info.size
})
return { startSize, endSize, startInfos, infos, endInfos }
}
const virtualScrollSlice = ({
size,
buffer,
startSize,
endSize,
startInfos,
infos,
endInfos,
pos,
isScroll,
barSize
}) => {
const list = []
const viewStart = pos + startSize - buffer
const viewEnd = pos + size - endSize + buffer - (isScroll ? barSize : 0)
startInfos.forEach((info) => list.push(info))
let firstInfo, firstOffset
for (let i = 0; i < infos.length; i++) {
const info = infos[i]
const itemStart = info.start
const itemEnd = itemStart + info.size
if (itemEnd < viewStart) {
continue
}
if ((itemStart >= viewStart && itemStart < viewEnd) || (itemEnd > viewStart && itemEnd <= viewEnd)) {
list.push(info)
if (!firstInfo) {
firstInfo = info
firstOffset = itemStart - (pos + startSize)
}
info.offset = firstOffset
} else if (firstInfo) {
break
}
}
endInfos.forEach((info) => list.push(info))
return list
}
export const createVirtualScroll =
({ api, props, state }) =>
() => {
let { width, height, columns, columnSizes, rows, rowSizes, columnBuffer, rowBuffer, fixedColumns, fixedRows } =
props
let { scrollbarSize } = state
width = +width
height = +height
columnBuffer = +columnBuffer
rowBuffer = +rowBuffer
const { isScrollX, isScrollY, columnTotal, rowTotal } = api.analyzeScrolling()
const boxStyle = { width: `${width}px`, height: `${height}px`, position: 'relative', overflow: 'auto' }
const xAxisStyle = { width: `${columnTotal}px`, height: '1px' }
const yAxisStyle = { height: `${rowTotal - 1}px`, width: '1px' }
const viewBaseStyle = { width: '100%', height: '100%', position: 'absolute', overflow: 'clip' }
const {
startSize: colStartSize,
endSize: colEndSize,
startInfos: colStartInfos,
infos: colInfos,
endInfos: colEndInfos
} = preprocessVirtualScrollSlice({ items: columns, itemSizes: columnSizes, fixedItems: fixedColumns })
const {
startSize: rowStartSize,
endSize: rowEndSize,
startInfos: rowStartInfos,
infos: rowInfos,
endInfos: rowEndInfos
} = preprocessVirtualScrollSlice({ items: rows, itemSizes: rowSizes, fixedItems: fixedRows })
return {
sliceColumn: (scrollLeft) =>
virtualScrollSlice({
pos: scrollLeft,
size: width,
buffer: columnBuffer,
startSize: colStartSize,
endSize: colEndSize,
startInfos: colStartInfos,
infos: colInfos,
endInfos: colEndInfos,
isScroll: isScrollY,
barSize: scrollbarSize
}),
sliceRow: (scrollTop) =>
virtualScrollSlice({
pos: scrollTop,
size: height,
buffer: rowBuffer,
startSize: rowStartSize,
endSize: rowEndSize,
startInfos: rowStartInfos,
infos: rowInfos,
endInfos: rowEndInfos,
isScroll: isScrollX,
barSize: scrollbarSize
}),
buildView: ({ slicedCols, slicedRows, scrollLeft, scrollTop }) => {
const viewRows = []
const viewCols = []
const result = {
isScrollX,
isScrollY,
boxStyle,
xAxisStyle,
yAxisStyle,
viewStyle: { left: `${scrollLeft}px`, top: `${scrollTop}px`, ...viewBaseStyle },
viewRows,
viewCols
}
let rowWidth = 0
slicedCols.forEach((info) => {
rowWidth += info.size
viewCols.push({
info,
key: `c-${info.index}`,
style: {
width: `${info.size}px`,
position: info.fixed ? 'sticky' : undefined,
zIndex: info.fixed ? 1 : undefined,
left: info.fixed === 'start' ? `${info.start}px` : undefined,
right: info.fixed === 'end' ? `${info.end}px` : undefined,
transform: info.offset === undefined ? undefined : `translateX(${info.offset}px)`
}
})
})
slicedRows.forEach((info) => {
viewRows.push({
info,
key: `r-${info.index}`,
style: {
width: `${rowWidth}px`,
height: `${info.size}px`,
position: info.fixed ? 'sticky' : undefined,
top: info.fixed === 'start' ? `${info.start}px` : undefined,
bottom: info.fixed === 'end' ? `${info.end}px` : undefined,
zIndex: info.fixed ? 2 : undefined,
transform: info.offset === undefined ? undefined : `translateY(${info.offset}px)`
}
})
})
return result
}
}
}
export const onScroll =
({ emit, state }) =>
(e, force = false) => {
const sLeft = e ? e.target.scrollLeft : 0
const sTop = e ? e.target.scrollTop : 0
const rangeThreshold = 5
if (state.scrollLeft !== sLeft || force) {
state.scrollLeft = sLeft
state.slicedCols = state.vs.sliceColumn(sLeft)
}
if (state.scrollTop !== sTop || force) {
state.scrollTop = sTop
state.slicedRows = state.vs.sliceRow(sTop)
}
state.ctx = state.vs.buildView({
slicedCols: state.slicedCols,
slicedRows: state.slicedRows,
scrollLeft: sLeft,
scrollTop: sTop
})
if (state.isReady) {
state.isTop = sTop < rangeThreshold
state.isBottom = sTop > state.maxTop - rangeThreshold
state.isLeft = sLeft < rangeThreshold
state.isRight = sLeft > state.maxLeft - rangeThreshold
}
state.slotParams = {
viewRows: state.ctx.viewRows,
viewCols: state.ctx.viewCols,
isScrollX: state.ctx.isScrollX,
isScrollY: state.ctx.isScrollY,
isTop: state.isTop,
isBottom: state.isBottom,
isLeft: state.isLeft,
isRight: state.isRight
}
emit('change', state.slotParams)
}
export const refresh =
({ api, state, vm, nextTick }) =>
(options = {}) => {
const { isKeepScrollTop = false, isKeepScrollLeft = false } = options || {}
const lastTop = isKeepScrollTop ? state.scrollTop : 0
const lastLeft = isKeepScrollLeft ? state.scrollLeft : 0
state.vs = api.createVirtualScroll()
api.onScroll({ target: { scrollLeft: lastLeft, scrollTop: lastTop } }, true)
nextTick(() => {
state.maxTop = vm.$el.scrollHeight - vm.$el.offsetHeight
state.maxLeft = vm.$el.scrollWidth - vm.$el.offsetWidth
vm.$el.scrollTop = lastTop
vm.$el.scrollLeft = lastLeft
})
}