import { Component, NewState, Util, Hash } from '@web/state' export const FastListComponent = Component.register('FastList', container => { const itemHeights = new Map() const groupHeights = new Map() let groupItemCount = 1 container.state.renderedList = [] const avg = Util.newAvg() let containerPaddingTop = 0 let containerRowGap = 0 let topMargin = 0 let itemMarginTop = null let itemMarginBottom = null let listStartIndex = 0 let visibleStartIndex = 0 let visibleCount = 10 let listInited = false container.onItemUpdate = (index, node) => { const absoluteIndex = index + listStartIndex 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 + containerRowGap 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) } } container.refresh = () => { if (!listInited) return visibleCount = Math.ceil((container.clientHeight || 100) / (avg.get() || 32)) let list = [] const fullList = container.state.list const fullSize = fullList.length let i = 0 let prev = containerPaddingTop + topMargin + containerRowGap let post = 0 let starus = 0 for (i = 0; i < fullSize; i++) { if (starus === 0) { const gh = groupHeights.get(i) if (gh && prev + gh < container.scrollTop) { prev += gh i += Math.min(groupItemCount, fullSize - i) - 1 } else { const ih = itemHeights.get(i) if (prev + ih < container.scrollTop) { prev += ih } else { starus = 1 visibleStartIndex = Math.max(0, i) listStartIndex = Math.max(0, visibleStartIndex - visibleCount) const listEndIndex = Math.min(listStartIndex + visibleCount * 3, fullSize) i = listEndIndex - 1 list = fullList.slice(listStartIndex, listEndIndex) for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j) } } } else if (starus === 1) { const gh = groupHeights.get(i) if (gh) { post += gh i += groupItemCount - 1 } else { post += itemHeights.get(i) } } } container.state.prevHeight = prev - containerPaddingTop - topMargin - containerRowGap container.state.postHeight = post container.state.renderedList = list } container.state.__watch('list', list => { listInited = false itemHeights.clear() groupHeights.clear() avg.clear() topMargin = 0 itemMarginTop = null itemMarginBottom = null if (!list?.length) return const listSize = list.length || 0 groupItemCount = Math.ceil(Math.sqrt(listSize)) || 10 const style = window.getComputedStyle(container) containerPaddingTop = parseFloat(style.paddingTop) || 0 containerRowGap = parseFloat(style.rowGap) || 0 container.state.renderedList = list.slice(0, Math.min(30, listSize)) || [] requestAnimationFrame(() => { if (listInited || container.state.list !== list) return const defaultHeight = avg.get() || 32 for (let i = 0; i < listSize; i++) { if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight) } for (let i = 0; i < listSize; i += groupItemCount) { let sum = 0 for (let j = i; j < Math.min(i + groupItemCount, listSize); j++) { sum += itemHeights.get(j) } groupHeights.set(i, sum) } listInited = true 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*/`
`))