import { Component, NewState, Util, Hash } from '@web/state'
export const VirtualScroll = () => {
const itemHeights = new Map()
const groupHeights = new Map()
let groupItemCount = 1
const avg = Util.newAvg()
let padTop = 0
let rowGap = 0
let topMargin = 0
let itemMarginTop = null
let itemMarginBottom = null
let listInited = false
return {
// 核心1:重置与首屏预渲染
reset: (list, container) => {
listInited = false
itemHeights.clear()
groupHeights.clear()
avg.clear()
topMargin = 0
itemMarginTop = null
itemMarginBottom = null
if (!list?.length) return []
const size = list.length
groupItemCount = Math.ceil(Math.sqrt(size)) || 10
const style = window.getComputedStyle(container)
padTop = parseFloat(style.paddingTop) || 0
rowGap = parseFloat(style.rowGap) || 0
return list.slice(0, Math.min(30, size))
},
// 核心2:无痛初始化缓存 (通过 rAF 调用)
init: (list, refreshCallback) => {
if (listInited) return
const size = list.length
const defaultHeight = avg.get() || 32
for (let i = 0; i < size; i++) {
if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight)
}
for (let i = 0; i < size; i += groupItemCount) {
let sum = 0
for (let j = i; j < Math.min(i + groupItemCount, size); j++) {
sum += itemHeights.get(j)
}
groupHeights.set(i, sum)
}
listInited = true
refreshCallback()
},
// 核心3:原汁原味的极速高度更新
update: (absoluteIndex, node) => {
if (itemMarginTop === null) {
const style = window.getComputedStyle(node)
itemMarginTop = parseFloat(style.marginTop) || 0
itemMarginBottom = parseFloat(style.marginBottom) || 0
}
if (absoluteIndex === 0) topMargin = itemMarginTop
const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap
const oldHeight = itemHeights.get(absoluteIndex)
if (newHeight !== oldHeight) {
itemHeights.set(absoluteIndex, newHeight)
avg.add(newHeight)
const offset = newHeight - (oldHeight || 0)
const groupIndex = absoluteIndex - (absoluteIndex % groupItemCount)
if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset)
}
},
// 核心4:原汁原味的状态机滚动计算
calc: (container, list) => {
if (!listInited || !list) return null
const size = list.length
const visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32))
let prev = padTop + topMargin + rowGap
let post = 0
let status = 0
let listStartIndex = 0
let listEndIndex = 0
for (let i = 0; i < size; i++) {
if (status === 0) {
const gh = groupHeights.get(i)
if (gh && prev + gh < container.scrollTop) {
prev += gh
i += Math.min(groupItemCount, size - i) - 1
} else {
const ih = itemHeights.get(i)
if (prev + ih < container.scrollTop) {
prev += ih
} else {
status = 1
let visibleStartIndex = Math.max(0, i)
listStartIndex = Math.max(0, visibleStartIndex - visibleCount)
listEndIndex = Math.min(listStartIndex + visibleCount * 3, size)
i = listEndIndex - 1
for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j)
}
}
} else if (status === 1) {
const gh = groupHeights.get(i)
if (gh) {
post += gh
i += groupItemCount - 1
} else {
post += itemHeights.get(i)
}
}
}
return {
prevHeight: prev - padTop - topMargin - rowGap,
postHeight: post,
renderedList: list.slice(listStartIndex, listEndIndex),
listStartIndex
}
}
}
}
export const FastListComponent = Component.register('FastList', container => {
const vs = VirtualScroll()
container.state.renderedList = []
container.onItemUpdate = (index, node) => {
vs.update(index + (container.state._listStartIndex || 0), node)
}
container.refresh = () => {
const res = vs.calc(container, container.state.list)
if (res) {
container.state.prevHeight = res.prevHeight
container.state.postHeight = res.postHeight
container.state._listStartIndex = res.listStartIndex
container.state.renderedList = res.renderedList
}
}
container.state.__watch('list', list => {
container.state._listStartIndex = 0
container.state.renderedList = vs.reset(list, container) || []
requestAnimationFrame(() => {
if (container.state.list !== list) return
vs.init(list, container.refresh)
})
})
}, Util.makeDom(/*html*/`
0" $style="height:\${this.state.prevHeight}px;" class="flex-shrink-0">
0" $style="height:\${this.state.postHeight}px;" class="flex-shrink-0">
`))
Component.attachSelectFeature = (container, eventName = 'itemclick', selectActionName = 'selectItem', activeActionName = 'itemActiveTag', selectedItemKey = 'selectedItem', idfieldKey = 'idfield', selectableAttr = 'auto-select') => {
const selectable = selectableAttr && container.hasAttribute(selectableAttr)
container[selectActionName] = (item, index) => {
if (selectable) container.state[selectedItemKey] = container.state[selectedItemKey] === item[container[idfieldKey]] ? null : item[container[idfieldKey]]
container.dispatchEvent(new CustomEvent(eventName, { bubbles: false, detail: !selectable || container.state[selectedItemKey] ? { item, index } : { item: {}, index: null } }))
}
container[activeActionName] = (item) => container.state[selectedItemKey] !== null && container.state[selectedItemKey] === item[container[idfieldKey]] ? 'active' : ''
}
Component.register('List', container => {
Component.attachSelectFeature(container)
const defaultSets = { idfield: 'id', labelfield: 'label', summaryfield: 'summary' }
container.state.__watch('list', oldList => {
Util.updateDefaults(container, defaultSets)
})
}, Util.makeDom(/*html*/`
\${item[this.labelfield]}
`))
Component.register('GroupedList', container => {
Component.getSetupFunction('List')(container)
container._selectItem = container.selectItem
container.selectItem = (item, index) => {
container.state.selectedGroup = null
container._selectItem(item, index)
}
Component.attachSelectFeature(container, 'groupclick', 'selectGroup', 'groupActiveTag', 'selectedGroup', 'groupidfield', container.hasAttribute('auto-select-group') || container.hasAttribute('auto-select') && 'group-selectable')
container._selectGroup = container.selectGroup
container.selectGroup = (item, index) => {
container.state.selectedItem = null
container._selectGroup(item, index)
}
const defaultSets = { typefield: 'type', groupidfield: 'id', grouplabelfield: 'label', groupsummaryfield: 'summary', groupfield: 'group' }
container.state.__watch('list', oldList => {
Util.updateDefaults(container, defaultSets)
const newList = [];
(container.state.groups || []).forEach(group => {
newList.push({ [container.typefield]: 'group', ...group })
if (oldList) oldList.forEach(item => {
if (item[container.groupfield] === group[container.groupidfield]) {
newList.push({ [container.typefield]: 'item', ...item })
}
})
})
return newList
})
}, Util.makeDom(/*html*/`
\${item[this.grouplabelfield]}
\${item[this.labelfield]}
`))
Component.register('FastGroupedList', container => {
Component.getSetupFunction('List')(container)
Component.getSetupFunction('GroupedList')(container)
container.refresh = () => container.querySelector('FastList')?.refresh()
}, Util.makeDom(/*html*/`
\${item[this.grouplabelfield]}
\${item[this.labelfield]}
`))
Component.register('Tree', container => {
const defaultSets = { parentfield: 'parent', groupicon: 'folder', itemicon: 'file' }
container.state.__watch('list', () => {
Util.updateDefaults(container, defaultSets)
const childrenMap = {};
(container.state.list || []).forEach(item => (childrenMap[item[container.parentfield] || ''] ??= []).push(item))
const newList = []
const traverse = (list, level) => list.forEach(item => {
const id = item[container.idfield]
const hasChildren = !!childrenMap[id]?.length
newList.push({ ...item, _level: level, _hasChildren: hasChildren })
if (hasChildren) traverse(childrenMap[id], level + 1)
})
traverse(childrenMap[''] || [], 0)
return newList
})
}, Util.makeDom(/*html*/`
`))
export const FastTreeComponent = Component.register('FastTree', container => {
Component.getSetupFunction('List')(container)
Component.getSetupFunction('Tree')(container)
container.refresh = () => container.querySelector('FastList')?.refresh()
}, Util.makeDom(/*html*/`
`))
Component.register('CollapseTree', container => {
Component.attachSelectFeature(container)
const defaultSets = { idfield: 'id', parentfield: 'parent', labelfield: 'label', summaryfield: 'summary' }
container.collapsed = NewState({})
container.state.__watch('list', () => {
Util.updateDefaults(container, defaultSets)
const childrenMap = {};
(container.state.list || []).forEach(item => (childrenMap[item[container.parentfield] || ''] ??= []).push(item))
const newList = []
const traverse = (list, level, parents) => list.forEach(item => {
const id = item[container.idfield]
const hasChildren = !!childrenMap[id]?.length
newList.push({ ...item, _level: level, _hasChildren: hasChildren, _parents: parents })
if (hasChildren) traverse(childrenMap[id], level + 1, [...parents, id])
})
traverse(childrenMap[''] || [], 0, [])
return newList
})
}, Util.makeDom(/*html*/`
this.collapsed[cur])" $class="list-group-item list-group-item-action d-flex ps-2 align-items-center \${this.itemActiveTag?.(item)}" $onclick="this.selectItem(item,index)">
`))