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*/`
`)) 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*/`
`)) Component.register('FastGroupedList', container => { Component.getSetupFunction('List')(container) Component.getSetupFunction('GroupedList')(container) }, 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) }, 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*/`
`))