2026-05-14 20:04:31 +08:00
|
|
|
|
import { Component, NewState, Util, Hash } from '@web/state'
|
|
|
|
|
|
|
2026-05-17 16:59:44 +08:00
|
|
|
|
export const VirtualScroll = () => {
|
2026-05-14 20:04:31 +08:00
|
|
|
|
const itemHeights = new Map()
|
|
|
|
|
|
const groupHeights = new Map()
|
|
|
|
|
|
let groupItemCount = 1
|
|
|
|
|
|
const avg = Util.newAvg()
|
2026-05-17 16:59:44 +08:00
|
|
|
|
|
|
|
|
|
|
let padTop = 0
|
|
|
|
|
|
let rowGap = 0
|
2026-05-14 20:04:31 +08:00
|
|
|
|
let topMargin = 0
|
2026-05-14 23:13:04 +08:00
|
|
|
|
let itemMarginTop = null
|
|
|
|
|
|
let itemMarginBottom = null
|
2026-05-14 20:04:31 +08:00
|
|
|
|
let listInited = false
|
|
|
|
|
|
|
2026-05-17 16:59:44 +08:00
|
|
|
|
return {
|
|
|
|
|
|
// 核心1:重置与首屏预渲染
|
|
|
|
|
|
reset: (list, container) => {
|
|
|
|
|
|
listInited = false
|
|
|
|
|
|
itemHeights.clear()
|
|
|
|
|
|
groupHeights.clear()
|
|
|
|
|
|
avg.clear()
|
|
|
|
|
|
topMargin = 0
|
|
|
|
|
|
itemMarginTop = null
|
|
|
|
|
|
itemMarginBottom = null
|
2026-05-14 20:04:31 +08:00
|
|
|
|
|
2026-05-17 16:59:44 +08:00
|
|
|
|
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
|
2026-05-14 23:13:04 +08:00
|
|
|
|
const defaultHeight = avg.get() || 32
|
2026-05-17 16:59:44 +08:00
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < size; i++) {
|
2026-05-14 23:13:04 +08:00
|
|
|
|
if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight)
|
|
|
|
|
|
}
|
2026-05-17 16:59:44 +08:00
|
|
|
|
for (let i = 0; i < size; i += groupItemCount) {
|
2026-05-14 23:13:04 +08:00
|
|
|
|
let sum = 0
|
2026-05-17 16:59:44 +08:00
|
|
|
|
for (let j = i; j < Math.min(i + groupItemCount, size); j++) {
|
2026-05-14 23:13:04 +08:00
|
|
|
|
sum += itemHeights.get(j)
|
|
|
|
|
|
}
|
|
|
|
|
|
groupHeights.set(i, sum)
|
|
|
|
|
|
}
|
2026-05-14 20:04:31 +08:00
|
|
|
|
listInited = true
|
2026-05-17 16:59:44 +08:00
|
|
|
|
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)
|
2026-05-14 20:04:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
}, Util.makeDom(/*html*/`
|
|
|
|
|
|
<div class="overflow-auto" onscroll="this.refresh()" style="overflow-anchor:none">
|
|
|
|
|
|
<div $if="this.state.prevHeight>0" $style="height:\${this.state.prevHeight}px;" class="flex-shrink-0"></div>
|
|
|
|
|
|
<div slot-id="item" class="flex-shrink-0" $each="this.state.renderedList" $onupdate="this.onItemUpdate(index,thisNode)"></div>
|
|
|
|
|
|
<div $if="this.state.postHeight>0" $style="height:\${this.state.postHeight}px;" class="flex-shrink-0"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`))
|
|
|
|
|
|
|
|
|
|
|
|
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*/`
|
|
|
|
|
|
<div class="list-group overflow-auto">
|
|
|
|
|
|
<div slot="item" slot-id="item" $each="this.state.list" $onclick="this.selectItem(item,index)" $class="list-group-item list-group-item-action list-group-item-heading d-inline-flex pe-auto align-items-baseline \${this.itemActiveTag?.(item)}">
|
|
|
|
|
|
<span $if="this.itemicon" class="bi bi-\${this.itemicon} text-body"></span>
|
|
|
|
|
|
<div class="flex-shrink-0 px-1" $text>\${item[this.labelfield]}</div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
|
|
|
|
|
|
<div slot-id="item-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`))
|
|
|
|
|
|
|
|
|
|
|
|
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*/`
|
|
|
|
|
|
<div class="list-group overflow-auto">
|
|
|
|
|
|
<template $each="this.state.list">
|
|
|
|
|
|
<div slot-id="group" $if="item.type === 'group'" $onclick="this.selectGroup(item,index)" style="height: 36px" $class="list-group-item list-group-item-action small d-inline-flex align-items-center ps-2 pe-2 \${this.groupActiveTag(item)}">
|
|
|
|
|
|
<span $if="this.groupicon" $class="bi bi-\${this.groupicon} text-body"></span>
|
|
|
|
|
|
<div class="flex-shrink-0 px-1" $text>\${item[this.grouplabelfield]}</div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.groupsummaryfield]"></div>
|
|
|
|
|
|
<div slot-id="group-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div slot-id="item" $if="item.type === 'item'" $onclick="this.selectItem(item,index)" $class="list-group-item list-group-item-action d-inline-flex align-items-baseline ps-3 pe-2 \${this.itemActiveTag?.(item)}">
|
|
|
|
|
|
<span $if="this.itemicon" $class="bi bi-\${this.itemicon} text-body"></span>
|
|
|
|
|
|
<div class="flex-shrink-0 px-1" $text>\${item[this.labelfield]}</div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
|
|
|
|
|
|
<div slot-id="item-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`))
|
|
|
|
|
|
|
|
|
|
|
|
Component.register('FastGroupedList', container => {
|
|
|
|
|
|
Component.getSetupFunction('List')(container)
|
|
|
|
|
|
Component.getSetupFunction('GroupedList')(container)
|
2026-05-18 20:23:05 +08:00
|
|
|
|
container.refresh = () => container.querySelector('FastList')?.refresh()
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}, Util.makeDom(/*html*/`
|
2026-05-18 20:23:05 +08:00
|
|
|
|
<FastList class="list-group" $.state.list="this.state.list">
|
2026-05-14 20:04:31 +08:00
|
|
|
|
<div slot="item">
|
|
|
|
|
|
<div slot-id="group" $if="item.type === 'group'" $onclick="this.selectGroup(item,index)" style="height: 36px" $class="list-group-item list-group-item-action small d-inline-flex align-items-center ps-2 pe-2 \${this.groupActiveTag(item)}">
|
|
|
|
|
|
<span $if="this.groupicon" $class="bi bi-\${this.groupicon} text-body"></span>
|
|
|
|
|
|
<div class="flex-shrink-0 px-1" $text>\${item[this.grouplabelfield]}</div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.groupsummaryfield]"></div>
|
|
|
|
|
|
<div slot-id="group-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div slot-id="item" $if="item.type === 'item'" $onclick="this.selectItem(item,index)" $class="list-group-item list-group-item-action d-inline-flex align-items-baseline ps-3 pe-2 \${this.itemActiveTag?.(item)}">
|
|
|
|
|
|
<span $if="this.itemicon" $class="bi bi-\${this.itemicon} text-body"></span>
|
|
|
|
|
|
<div class="flex-shrink-0 px-1" $text>\${item[this.labelfield]}</div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
|
|
|
|
|
|
<div slot-id="item-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</FastList>
|
|
|
|
|
|
`))
|
|
|
|
|
|
|
|
|
|
|
|
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*/`
|
|
|
|
|
|
<List>
|
|
|
|
|
|
<div slot="item" $class="list-group-item list-group-item-action d-flex ps-2 align-items-center \${this.itemActiveTag?.(item)}" $onclick="this.selectItem(item,index)">
|
|
|
|
|
|
<div $style="width:\${item._level * 16}px" class="flex-shrink-0"></div>
|
|
|
|
|
|
<div $class="text-muted bi bi-\${item._hasChildren?this.groupicon:this.itemicon}"></div>
|
|
|
|
|
|
<div class="flex-grow-1 text-truncate px-2" $text="item[this.labelfield]"></div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
|
|
|
|
|
|
<div slot-id="item-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</List>
|
|
|
|
|
|
`))
|
|
|
|
|
|
|
|
|
|
|
|
export const FastTreeComponent = Component.register('FastTree', container => {
|
|
|
|
|
|
Component.getSetupFunction('List')(container)
|
|
|
|
|
|
Component.getSetupFunction('Tree')(container)
|
2026-05-18 20:23:05 +08:00
|
|
|
|
container.refresh = () => container.querySelector('FastList')?.refresh()
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}, Util.makeDom(/*html*/`
|
2026-05-18 20:23:05 +08:00
|
|
|
|
<FastList class="list-group list-group-action" $.state.list="this.state.list">
|
2026-05-14 20:04:31 +08:00
|
|
|
|
<div slot="item" $class="list-group-item list-group-item-action d-flex ps-2 align-items-center \${this.itemActiveTag?.(item)}" $onclick="this.selectItem(item,index)">
|
|
|
|
|
|
<div $style="width:\${item._level * 16}px" class="flex-shrink-0"></div>
|
|
|
|
|
|
<div $class="text-muted bi bi-\${item._hasChildren?this.groupicon:this.itemicon}"></div>
|
|
|
|
|
|
<div class="flex-grow-1 text-truncate px-2" $text="item[this.labelfield]"></div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
|
|
|
|
|
|
<div slot-id="item-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</FastList>
|
|
|
|
|
|
`))
|
|
|
|
|
|
|
|
|
|
|
|
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*/`
|
|
|
|
|
|
<List>
|
|
|
|
|
|
<div slot="item" slot-id="item" $if="!item._parents.some(cur=>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)">
|
|
|
|
|
|
<div $style="width:\${item._level * 16 + 16}px; cursor: pointer;" class="text-end text-muted flex-shrink-0" $onclick="event.stopPropagation();this.collapsed[item[this.idfield]] = !this.collapsed[item[this.idfield]]">
|
|
|
|
|
|
<i $if="item._hasChildren" $class="bi \${this.collapsed[item[this.idfield]] ? 'bi-caret-right-fill' : 'bi-caret-down-fill'}"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex-grow-1 text-truncate px-1" $text="item[this.labelfield]"></div>
|
|
|
|
|
|
<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="item[this.summaryfield]"></div>
|
|
|
|
|
|
<div slot-id="item-actions"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</List>
|
|
|
|
|
|
`))
|