refactor(base): 重构文档和列表导航(by AI)

This commit is contained in:
AI Engineer 2026-07-07 00:17:16 +08:00
parent 2da203c513
commit 4b010c88c2
11 changed files with 1239 additions and 661 deletions

View File

@ -250,8 +250,6 @@
- **`collapsible`** (Boolean): 仅在 `tree` 模式下有效,开启树形节点的展开/收起能力。
- **`auto-select`** (Boolean): 开启后,点击列表项时自动将该项的 `id` 记录至 `state.selectedItem`,如果重复点击则会置空。
- **`auto-select-group`** (Boolean): 开启后,点击分组时自动将分组的 `id` 记录至 `state.selectedGroup`
- **`filter`** (String): 筛选关键字。支持通过 attribute 传入以自动同步至 `state.filter`
- **`order`** (String): 排序规则。支持通过 attribute 传入以自动同步至 `state.order`
### 4. 内部状态模型 (`State`)
- `list` (读写): 原始列表数据数组。
@ -259,11 +257,15 @@
- `collapsed` (读写): 仅在 `tree``collapsible` 时使用,存储节点 ID 折叠状态的 Map Proxy。
- `selectedItem` (读写): 当前选中的列表项 ID。
- `selectedGroup` (读写): 当前选中的分组 ID。
- `filter` (读写): 筛选关键字(进行 label 和 summary 的不区分大小写模糊匹配)或自定义过滤函数 `(item) => boolean`
- `order` (读写): 排序规则。可为排序字段名(例如 `'name'`;前缀 `-` 表示降序,如 `'-name'`;或者后缀 `'desc'`/`'asc'` 语法,如 `'name desc'`),也可以为自定义排序函数 `(a, b) => number`
- `filter` (读写): 筛选规则。可为字符串(对 label 和 summary 进行忽略大小写的模糊包含匹配)或对象 `{ [key]: query }`(对对象特定字段进行模糊包含匹配)
- `order` (读写): 排序规则。可为字段名字符串(例如 `'name'`支持前缀 `-` 表示降序,如 `'-name'`;或者后缀 `'desc'`/`'asc'` 语法,如 `'name desc'`)。
- `_flatList` (只读): 列表核心逻辑处理后的扁平化数组。
- `_renderedList` (只读): 实际在 DOM 中遍历渲染的数组片段(若开启虚拟滚动,则只包含可视区切片)。
### 5. 实例属性 (DOM Node Properties)
- `filterFunc` (读写): 自定义过滤函数 `(item) => boolean`。设置后会重绘列表。
- `orderFunc` (读写): 自定义排序比较函数 `(a, b) => number`。设置后会重绘列表。
### 5. 插槽 (Slots)
- **`item`**:列表单项渲染模板插槽。在自定义模板中,可以访问当前行数据 `item` 和索引 `index`
- **`item-actions`**:行数据右侧按钮工具栏区域。

1053
README.md

File diff suppressed because it is too large Load Diff

171
dist/base.js vendored
View File

@ -761,16 +761,48 @@
};
container.collapsed = globalThis.NewState({});
container.state.renderedList = [];
container.addEventListener("bind", (e) => {
container.state.selectedItem = e.detail;
});
const matchString = (val, query) => {
if (query === void 0 || query === null || query === "") return true;
return String(val || "").toLowerCase().includes(String(query).toLowerCase());
};
const matchesItem = (item, filter, filterFunc) => {
if (filterFunc && !filterFunc(item)) return false;
if (filter === 0) filter = "";
if (filter !== void 0 && filter !== null && filter !== "") {
if (typeof filter === "object") {
for (const [key, qVal] of Object.entries(filter)) {
if (qVal !== void 0 && qVal !== null && qVal !== "") {
if (!matchString(item[key], qVal)) return false;
}
}
} else {
const q = String(filter).trim();
if (q) {
const labelMatches = matchString(item[container.labelfield], q);
const summaryMatches = matchString(item[container.summaryfield], q);
if (!labelMatches && !summaryMatches) return false;
}
}
}
return true;
};
const updateFlatList = () => {
var _a;
globalThis.Util.updateDefaults(container, defaultSets);
let list = container.state.list || [];
const filter = container.state.filter;
const order = container.state.order;
if (order) {
if (typeof order === "function") {
list = [...list].sort(order);
} else if (typeof order === "string") {
let filter = container.state.filter;
let order = container.state.order;
const filterFunc = container._filterFunc;
const orderFunc = container._orderFunc;
if (filter === 0) filter = "";
if (order === 0) order = "";
if (orderFunc) {
list = [...list].sort(orderFunc);
} else if (order) {
if (typeof order === "string") {
const parts = order.trim().split(/\s+/);
const field = parts[0];
const isDesc = ((_a = parts[1]) == null ? void 0 : _a.toLowerCase()) === "desc" || field.startsWith("-");
@ -789,21 +821,7 @@
}
const flatList = [];
if (container.mode === "group") {
let filteredList = list;
if (filter) {
if (typeof filter === "function") {
filteredList = list.filter(filter);
} else if (typeof filter === "string") {
const q = filter.trim().toLowerCase();
if (q) {
filteredList = list.filter((item) => {
const label = String(item[container.labelfield] || "").toLowerCase();
const summary = String(item[container.summaryfield] || "").toLowerCase();
return label.includes(q) || summary.includes(q);
});
}
}
}
const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
const itemMap = {};
filteredList.forEach((item) => {
var _a2;
@ -811,11 +829,26 @@
});
(container.state.groups || []).forEach((group) => {
const items = itemMap[group[container.groupidfield]];
const groupLabel = String(group[container.grouplabelfield] || "").toLowerCase();
const groupSummary = String(group[container.groupsummaryfield] || "").toLowerCase();
const q = typeof filter === "string" ? filter.trim().toLowerCase() : "";
const groupMatches = q ? groupLabel.includes(q) || groupSummary.includes(q) : false;
if (items && items.length > 0 || groupMatches || !filter) {
let groupMatches = false;
if (filter !== void 0 && filter !== null && filter !== "") {
if (typeof filter === "object") {
groupMatches = true;
for (const [key, qVal] of Object.entries(filter)) {
if (qVal !== void 0 && qVal !== null && qVal !== "") {
if (!matchString(group[key], qVal)) {
groupMatches = false;
break;
}
}
}
} else {
const q = String(filter).trim();
if (q) {
groupMatches = matchString(group[container.grouplabelfield], q) || matchString(group[container.groupsummaryfield], q);
}
}
}
if (items && items.length > 0 || groupMatches || !filter && !filterFunc) {
flatList.push({ type: "group", ...group });
if (items) items.forEach((item) => flatList.push({ type: "item", ...item }));
}
@ -827,27 +860,12 @@
});
const matches = /* @__PURE__ */ new Set();
list.forEach((item) => {
let match = false;
if (!filter) {
match = true;
} else if (typeof filter === "function") {
match = filter(item);
} else if (typeof filter === "string") {
const q = filter.trim().toLowerCase();
if (!q) {
match = true;
} else {
const label = String(item[container.labelfield] || "").toLowerCase();
const summary = String(item[container.summaryfield] || "").toLowerCase();
match = label.includes(q) || summary.includes(q);
}
}
if (match) {
if (matchesItem(item, filter, filterFunc)) {
matches.add(item[container.idfield]);
}
});
const keep = new Set(matches);
if (filter) {
if (filter || filterFunc) {
matches.forEach((id) => {
let curr = itemMap[id];
while (curr) {
@ -861,7 +879,7 @@
}
const childrenMap = {};
list.forEach((item) => {
if (!filter || keep.has(item[container.idfield])) {
if (!filter && !filterFunc || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || "";
(childrenMap[parentId] ?? (childrenMap[parentId] = [])).push(item);
}
@ -876,21 +894,7 @@
});
traverse(childrenMap[""] || [], 0, []);
} else {
let filteredList = list;
if (filter) {
if (typeof filter === "function") {
filteredList = list.filter(filter);
} else if (typeof filter === "string") {
const q = filter.trim().toLowerCase();
if (q) {
filteredList = list.filter((item) => {
const label = String(item[container.labelfield] || "").toLowerCase();
const summary = String(item[container.summaryfield] || "").toLowerCase();
return label.includes(q) || summary.includes(q);
});
}
}
}
const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
filteredList.forEach((item) => flatList.push({ type: "item", ...item }));
}
container.state.flatList = flatList;
@ -932,7 +936,10 @@
} else container.state.renderedList = flatList;
});
container.selectItem = (item, index2) => {
if (container.hasAttribute("auto-select")) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield];
if (container.hasAttribute("auto-select")) {
container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield];
}
container.dispatchEvent(new CustomEvent("change", { detail: container.state.selectedItem, bubbles: false }));
container.dispatchEvent(new CustomEvent("itemclick", { bubbles: false, detail: { item, index: index2 + (container.fast ? container.state.listStartIndex || 0 : 0) } }));
};
container.selectGroup = (item, index2) => {
@ -945,31 +952,20 @@
updateFlatList();
}
};
Object.defineProperty(container, "filter", {
get: () => container.state.filter,
Object.defineProperty(container, "filterFunc", {
get: () => container._filterFunc,
set: (v) => {
container.state.filter = v;
container._filterFunc = v;
updateFlatList();
}
});
Object.defineProperty(container, "order", {
get: () => container.state.order,
Object.defineProperty(container, "orderFunc", {
get: () => container._orderFunc,
set: (v) => {
container.state.order = v;
container._orderFunc = v;
updateFlatList();
}
});
const observer = new MutationObserver(() => {
const filterAttr = container.getAttribute("filter");
if (typeof container.state.filter !== "function" && filterAttr !== container.state.filter) {
container.state.filter = filterAttr || void 0;
}
const orderAttr = container.getAttribute("order");
if (typeof container.state.order !== "function" && orderAttr !== container.state.order) {
container.state.order = orderAttr || void 0;
}
});
observer.observe(container, { attributes: true, attributeFilter: ["filter", "order"] });
container.state.filter = container.getAttribute("filter") || void 0;
container.state.order = container.getAttribute("order") || void 0;
updateFlatList();
}, globalThis.Util.makeDom(
/*html*/
@ -1001,7 +997,7 @@
<span $class="bi bi-\${this.itemicon} text-body"></span>
</template>
<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div>
<div class="text-muted small flex-fill text-end" $text="\${item[this.summaryfield]}"></div>
<div class="text-muted fs-7 small ms-2 flex-fill text-truncate text-end" $text="\${item[this.summaryfield]}"></div>
<div slot-id="item-actions"></div>
</template>
</div>
@ -1013,11 +1009,20 @@
globalThis.Component.register("Nav", (container) => {
container.vertical = container.hasAttribute("vertical");
container.state.openName = container.state.openName || null;
container.state.value = container.state.value || null;
container.addEventListener("bind", (e) => {
container.state.value = e.detail;
});
container.click = (item, noselect) => {
if (!item.noselect && !noselect) globalThis.Hash.nav = item.name;
if (!item.noselect && !noselect) {
container.state.value = item.name;
container.dispatchEvent(new CustomEvent("change", { detail: item.name, bubbles: false }));
}
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
};
container.clickSubitem = (item) => {
container.state.value = item.name;
container.dispatchEvent(new CustomEvent("change", { detail: item.name, bubbles: false }));
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
};
container.toggleGroup = (item) => {
@ -1027,7 +1032,7 @@
}, globalThis.Util.makeDom(
/*html*/
`
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100 align-self-stretch overflow-visible' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-2 \${this.vertical ? 'py-3' : 'py-0'} align-items-center \${this.vertical ? 'align-items-start' : ''}" $style="this.vertical ? 'min-height:0;' : ''">
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100 align-self-stretch overflow-visible' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-2 \${this.vertical ? 'py-3' : 'py-1'} align-items-center \${this.vertical ? 'align-items-start' : ''}" $style="this.vertical ? 'min-height:0;' : ''">
<div class="d-flex align-items-center gap-2">
<template $if="this.state?.brand?.image">
<img $src="this.state.brand.image" $class="\${this.vertical ? 'mb-2' : 'mx-1'}" style="height:30px;width:auto;max-width:300px">
@ -1046,7 +1051,7 @@
<div $class="\${this.vertical ? 'small text-uppercase text-body-secondary fw-semibold px-2 py-1 mt-2' : 'navbar-text small text-uppercase text-body-secondary fw-semibold px-2'} \${item.class || ''}" $text="item.label"></div>
</template>
<template $if="item.type==='button'">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'w-100 text-start py-1 px-2' : ''} \${item.class || ''}" $onclick="this.click(item)">
<button $class="nav-link \${this.state.value===item.name?'active':''} \${this.vertical ? 'w-100 text-start py-1 px-2' : ''} \${item.class || ''}" $onclick="this.click(item)">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? 'text-truncate' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
</template>

2
dist/base.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "@apigo.cc/base",
"version": "1.0.22",
"version": "1.0.23",
"type": "module",
"main": "dist/base.js",
"files": [

View File

@ -2,225 +2,223 @@
* List Component Module
*/
globalThis.Component.register('List', container => {
container.mode = container.getAttribute('mode') || 'normal'
container.fast = container.hasAttribute('fast')
container.collapsible = container.hasAttribute('collapsible')
container.mode = container.getAttribute('mode') || 'normal'
container.fast = container.hasAttribute('fast')
container.collapsible = container.hasAttribute('collapsible')
const padTopEl = container.fast ? container.querySelector('.vs-pad-top') : null
const padBottomEl = container.fast ? container.querySelector('.vs-pad-bottom') : null
const padTopEl = container.fast ? container.querySelector('.vs-pad-top') : null
const padBottomEl = container.fast ? container.querySelector('.vs-pad-bottom') : null
const defaultSets = {
idfield: 'id', labelfield: 'label', summaryfield: 'summary',
groupidfield: 'id', grouplabelfield: 'label', groupsummaryfield: 'summary', groupfield: 'group',
parentfield: 'parent', groupicon: 'folder', itemicon: 'file'
}
container.collapsed = globalThis.NewState({})
container.state.renderedList = []
const defaultSets = {
idfield: 'id', labelfield: 'label', summaryfield: 'summary',
groupidfield: 'id', grouplabelfield: 'label', groupsummaryfield: 'summary', groupfield: 'group',
parentfield: 'parent', groupicon: 'folder', itemicon: 'file'
}
container.collapsed = globalThis.NewState({})
container.state.renderedList = []
const updateFlatList = () => {
globalThis.Util.updateDefaults(container, defaultSets)
let list = container.state.list || []
const filter = container.state.filter
const order = container.state.order
container.addEventListener('bind', e => {
container.state.selectedItem = e.detail
})
if (order) {
if (typeof order === 'function') {
list = [...list].sort(order)
} else if (typeof order === 'string') {
const parts = order.trim().split(/\s+/)
const field = parts[0]
const isDesc = parts[1]?.toLowerCase() === 'desc' || field.startsWith('-')
const cleanField = field.startsWith('-') ? field.slice(1) : field
list = [...list].sort((a, b) => {
const valA = a[cleanField]
const valB = b[cleanField]
if (valA === undefined || valA === null) return isDesc ? 1 : -1
if (valB === undefined || valB === null) return isDesc ? -1 : 1
if (typeof valA === 'string' && typeof valB === 'string') {
return isDesc ? valB.localeCompare(valA) : valA.localeCompare(valB)
}
return isDesc ? (valB > valA ? 1 : -1) : (valA > valB ? 1 : -1)
})
}
}
const matchString = (val, query) => {
if (query === undefined || query === null || query === '') return true
return String(val || '').toLowerCase().includes(String(query).toLowerCase())
}
const flatList = []
if (container.mode === 'group') {
let filteredList = list
if (filter) {
if (typeof filter === 'function') {
filteredList = list.filter(filter)
} else if (typeof filter === 'string') {
const q = filter.trim().toLowerCase()
if (q) {
filteredList = list.filter(item => {
const label = String(item[container.labelfield] || '').toLowerCase()
const summary = String(item[container.summaryfield] || '').toLowerCase()
return label.includes(q) || summary.includes(q)
})
}
}
}
const itemMap = {}
filteredList.forEach(item => (itemMap[item[container.groupfield]] ??= []).push(item));
(container.state.groups || []).forEach(group => {
const items = itemMap[group[container.groupidfield]]
const groupLabel = String(group[container.grouplabelfield] || '').toLowerCase()
const groupSummary = String(group[container.groupsummaryfield] || '').toLowerCase()
const q = (typeof filter === 'string') ? filter.trim().toLowerCase() : ''
const groupMatches = q ? (groupLabel.includes(q) || groupSummary.includes(q)) : false
const matchesItem = (item, filter, filterFunc) => {
if (filterFunc && !filterFunc(item)) return false
if (filter === 0) filter = ''
if ((items && items.length > 0) || groupMatches || !filter) {
flatList.push({ type: 'group', ...group })
if (items) items.forEach(item => flatList.push({ type: 'item', ...item }))
}
})
} else if (container.mode === 'tree') {
const itemMap = {}
list.forEach(item => { itemMap[item[container.idfield]] = item })
if (filter !== undefined && filter !== null && filter !== '') {
if (typeof filter === 'object') {
for (const [key, qVal] of Object.entries(filter)) {
if (qVal !== undefined && qVal !== null && qVal !== '') {
if (!matchString(item[key], qVal)) return false
}
}
} else {
const q = String(filter).trim()
if (q) {
const labelMatches = matchString(item[container.labelfield], q)
const summaryMatches = matchString(item[container.summaryfield], q)
if (!labelMatches && !summaryMatches) return false
}
}
}
return true
}
const matches = new Set()
list.forEach(item => {
let match = false
if (!filter) {
match = true
} else if (typeof filter === 'function') {
match = filter(item)
} else if (typeof filter === 'string') {
const q = filter.trim().toLowerCase()
if (!q) {
match = true
} else {
const label = String(item[container.labelfield] || '').toLowerCase()
const summary = String(item[container.summaryfield] || '').toLowerCase()
match = label.includes(q) || summary.includes(q)
}
}
if (match) {
matches.add(item[container.idfield])
}
})
const updateFlatList = () => {
globalThis.Util.updateDefaults(container, defaultSets)
let list = container.state.list || []
let filter = container.state.filter
let order = container.state.order
const filterFunc = container._filterFunc
const orderFunc = container._orderFunc
const keep = new Set(matches)
if (filter) {
matches.forEach(id => {
let curr = itemMap[id]
while (curr) {
const parentId = curr[container.parentfield]
if (!parentId) break
if (keep.has(parentId)) break
keep.add(parentId)
curr = itemMap[parentId]
}
})
}
if (filter === 0) filter = ''
if (order === 0) order = ''
const childrenMap = {}
list.forEach(item => {
if (!filter || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || ''
;(childrenMap[parentId] ??= []).push(item)
}
})
if (orderFunc) {
list = [...list].sort(orderFunc)
} else if (order) {
if (typeof order === 'string') {
const parts = order.trim().split(/\s+/)
const field = parts[0]
const isDesc = parts[1]?.toLowerCase() === 'desc' || field.startsWith('-')
const cleanField = field.startsWith('-') ? field.slice(1) : field
list = [...list].sort((a, b) => {
const valA = a[cleanField]
const valB = b[cleanField]
if (valA === undefined || valA === null) return isDesc ? 1 : -1
if (valB === undefined || valB === null) return isDesc ? -1 : 1
if (typeof valA === 'string' && typeof valB === 'string') {
return isDesc ? valB.localeCompare(valA) : valA.localeCompare(valB)
}
return isDesc ? (valB > valA ? 1 : -1) : (valA > valB ? 1 : -1)
})
}
}
const traverse = (items, level, parents) => items.forEach(item => {
const id = item[container.idfield]
const hasChildren = !!childrenMap[id]?.length
const isCollapsed = container.collapsed[id]
flatList.push({ type: 'item', ...item, _level: level, _hasChildren: hasChildren, _parents: parents })
if (hasChildren && !isCollapsed) traverse(childrenMap[id], level + 1, [...parents, id])
})
traverse(childrenMap[''] || [], 0, [])
} else {
let filteredList = list
if (filter) {
if (typeof filter === 'function') {
filteredList = list.filter(filter)
} else if (typeof filter === 'string') {
const q = filter.trim().toLowerCase()
if (q) {
filteredList = list.filter(item => {
const label = String(item[container.labelfield] || '').toLowerCase()
const summary = String(item[container.summaryfield] || '').toLowerCase()
return label.includes(q) || summary.includes(q)
})
}
}
}
filteredList.forEach(item => flatList.push({ type: 'item', ...item }))
}
container.state.flatList = flatList
}
const flatList = []
if (container.mode === 'group') {
const filteredList = list.filter(item => matchesItem(item, filter, filterFunc))
const itemMap = {}
filteredList.forEach(item => (itemMap[item[container.groupfield]] ??= []).push(item));
(container.state.groups || []).forEach(group => {
const items = itemMap[group[container.groupidfield]]
let groupMatches = false
if (filter !== undefined && filter !== null && filter !== '') {
if (typeof filter === 'object') {
groupMatches = true
for (const [key, qVal] of Object.entries(filter)) {
if (qVal !== undefined && qVal !== null && qVal !== '') {
if (!matchString(group[key], qVal)) {
groupMatches = false
break
}
}
}
} else {
const q = String(filter).trim()
if (q) {
groupMatches = matchString(group[container.grouplabelfield], q) ||
matchString(group[container.groupsummaryfield], q)
}
}
}
container.state.__watch('list', updateFlatList)
container.state.__watch('filter', updateFlatList)
container.state.__watch('order', updateFlatList)
const vs = container.fast ? globalThis.VirtualScroll() : null
if ((items && items.length > 0) || groupMatches || (!filter && !filterFunc)) {
flatList.push({ type: 'group', ...group })
if (items) items.forEach(item => flatList.push({ type: 'item', ...item }))
}
})
} else if (container.mode === 'tree') {
const itemMap = {}
list.forEach(item => { itemMap[item[container.idfield]] = item })
let refreshing = false
container.refresh = () => {
if (!container.fast || refreshing) return
refreshing = true
try {
const res = vs.calc(container, container.state.flatList)
if (res) {
if (padTopEl) padTopEl.style.height = `${res.prevHeight}px`
if (padBottomEl) padBottomEl.style.height = `${res.postHeight}px`
container.state.listStartIndex = res.listStartIndex
container.state.renderedList = res.renderedList
}
} finally {
setTimeout(() => { refreshing = false }, 0)
}
}
const matches = new Set()
list.forEach(item => {
if (matchesItem(item, filter, filterFunc)) {
matches.add(item[container.idfield])
}
})
container.onItemUpdate = (index, node) => { if (container.fast) vs.update(index + (container.state.listStartIndex || 0), node) }
const keep = new Set(matches)
if (filter || filterFunc) {
matches.forEach(id => {
let curr = itemMap[id]
while (curr) {
const parentId = curr[container.parentfield]
if (!parentId) break
if (keep.has(parentId)) break
keep.add(parentId)
curr = itemMap[parentId]
}
})
}
container.state.__watch('flatList', flatList => {
if (container.fast) {
if (padTopEl) padTopEl.style.height = '0px'
if (padBottomEl) padBottomEl.style.height = '0px'
container.state.listStartIndex = 0
container.state.renderedList = vs.reset(flatList, container) || []
setTimeout(() => { if (container.state.flatList === flatList) vs.init(flatList, container.refresh) })
} else container.state.renderedList = flatList
})
const childrenMap = {}
list.forEach(item => {
if ((!filter && !filterFunc) || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || ''
; (childrenMap[parentId] ??= []).push(item)
}
})
container.selectItem = (item, index) => {
if (container.hasAttribute('auto-select')) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield]
container.dispatchEvent(new CustomEvent('itemclick', { bubbles: false, detail: { item, index: index + (container.fast ? (container.state.listStartIndex || 0) : 0) } }))
}
container.selectGroup = (item, index) => {
if (container.hasAttribute('auto-select-group')) container.state.selectedGroup = container.state.selectedGroup === item[container.groupidfield] ? null : item[container.groupidfield]
container.dispatchEvent(new CustomEvent('groupclick', { bubbles: false, detail: { item, index } }))
}
container.toggleCollapse = (item) => { if (container.collapsible && item._hasChildren) { container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; updateFlatList(); } }
const traverse = (items, level, parents) => items.forEach(item => {
const id = item[container.idfield]
const hasChildren = !!childrenMap[id]?.length
const isCollapsed = container.collapsed[id]
flatList.push({ type: 'item', ...item, _level: level, _hasChildren: hasChildren, _parents: parents })
if (hasChildren && !isCollapsed) traverse(childrenMap[id], level + 1, [...parents, id])
})
traverse(childrenMap[''] || [], 0, [])
} else {
const filteredList = list.filter(item => matchesItem(item, filter, filterFunc))
filteredList.forEach(item => flatList.push({ type: 'item', ...item }))
}
container.state.flatList = flatList
}
Object.defineProperty(container, 'filter', {
get: () => container.state.filter,
set: v => { container.state.filter = v; }
})
Object.defineProperty(container, 'order', {
get: () => container.state.order,
set: v => { container.state.order = v; }
})
container.state.__watch('list', updateFlatList)
container.state.__watch('filter', updateFlatList)
container.state.__watch('order', updateFlatList)
const vs = container.fast ? globalThis.VirtualScroll() : null
const observer = new MutationObserver(() => {
const filterAttr = container.getAttribute('filter')
if (typeof container.state.filter !== 'function' && filterAttr !== container.state.filter) {
container.state.filter = filterAttr || undefined
}
const orderAttr = container.getAttribute('order')
if (typeof container.state.order !== 'function' && orderAttr !== container.state.order) {
container.state.order = orderAttr || undefined
}
})
observer.observe(container, { attributes: true, attributeFilter: ['filter', 'order'] })
let refreshing = false
container.refresh = () => {
if (!container.fast || refreshing) return
refreshing = true
try {
const res = vs.calc(container, container.state.flatList)
if (res) {
if (padTopEl) padTopEl.style.height = `${res.prevHeight}px`
if (padBottomEl) padBottomEl.style.height = `${res.postHeight}px`
container.state.listStartIndex = res.listStartIndex
container.state.renderedList = res.renderedList
}
} finally {
setTimeout(() => { refreshing = false }, 0)
}
}
container.state.filter = container.getAttribute('filter') || undefined
container.state.order = container.getAttribute('order') || undefined
container.onItemUpdate = (index, node) => { if (container.fast) vs.update(index + (container.state.listStartIndex || 0), node) }
updateFlatList()
container.state.__watch('flatList', flatList => {
if (container.fast) {
if (padTopEl) padTopEl.style.height = '0px'
if (padBottomEl) padBottomEl.style.height = '0px'
container.state.listStartIndex = 0
container.state.renderedList = vs.reset(flatList, container) || []
setTimeout(() => { if (container.state.flatList === flatList) vs.init(flatList, container.refresh) })
} else container.state.renderedList = flatList
})
container.selectItem = (item, index) => {
if (container.hasAttribute('auto-select')) {
container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield]
}
container.dispatchEvent(new CustomEvent('change', { detail: container.state.selectedItem, bubbles: false }))
container.dispatchEvent(new CustomEvent('itemclick', { bubbles: false, detail: { item, index: index + (container.fast ? (container.state.listStartIndex || 0) : 0) } }))
}
container.selectGroup = (item, index) => {
if (container.hasAttribute('auto-select-group')) container.state.selectedGroup = container.state.selectedGroup === item[container.groupidfield] ? null : item[container.groupidfield]
container.dispatchEvent(new CustomEvent('groupclick', { bubbles: false, detail: { item, index } }))
}
container.toggleCollapse = (item) => { if (container.collapsible && item._hasChildren) { container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; updateFlatList(); } }
Object.defineProperty(container, 'filterFunc', {
get: () => container._filterFunc,
set: v => { container._filterFunc = v; updateFlatList(); }
})
Object.defineProperty(container, 'orderFunc', {
get: () => container._orderFunc,
set: v => { container._orderFunc = v; updateFlatList(); }
})
updateFlatList()
}, globalThis.Util.makeDom(/*html*/`
<div class="list-group overflow-auto" onscroll="this.refresh()" style="overflow-anchor:none">
<div class="vs-pad-top flex-shrink-0" style="height:0px;"></div>
@ -249,7 +247,7 @@ globalThis.Component.register('List', container => {
<span $class="bi bi-\${this.itemicon} text-body"></span>
</template>
<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div>
<div class="text-muted small flex-fill text-end" $text="\${item[this.summaryfield]}"></div>
<div class="text-muted fs-7 small ms-2 flex-fill text-truncate text-end" $text="\${item[this.summaryfield]}"></div>
<div slot-id="item-actions"></div>
</template>
</div>

View File

@ -4,11 +4,22 @@
globalThis.Component.register('Nav', container => {
container.vertical = container.hasAttribute('vertical')
container.state.openName = container.state.openName || null
container.state.value = container.state.value || null
container.addEventListener('bind', e => {
container.state.value = e.detail
})
container.click = (item, noselect) => {
if (!item.noselect && !noselect) globalThis.Hash.nav = item.name
if (!item.noselect && !noselect) {
container.state.value = item.name
container.dispatchEvent(new CustomEvent('change', { detail: item.name, bubbles: false }))
}
container.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false }))
}
container.clickSubitem = item => {
container.state.value = item.name
container.dispatchEvent(new CustomEvent('change', { detail: item.name, bubbles: false }))
container.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false }))
}
container.toggleGroup = item => {
@ -16,7 +27,7 @@ globalThis.Component.register('Nav', container => {
container.dispatchEvent(new CustomEvent('nav', { detail: { item, open: container.state.openName === item.name }, bubbles: false }))
}
}, globalThis.Util.makeDom(/*html*/`
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100 align-self-stretch overflow-visible' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-2 \${this.vertical ? 'py-3' : 'py-0'} align-items-center \${this.vertical ? 'align-items-start' : ''}" $style="this.vertical ? 'min-height:0;' : ''">
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100 align-self-stretch overflow-visible' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-2 \${this.vertical ? 'py-3' : 'py-1'} align-items-center \${this.vertical ? 'align-items-start' : ''}" $style="this.vertical ? 'min-height:0;' : ''">
<div class="d-flex align-items-center gap-2">
<template $if="this.state?.brand?.image">
<img $src="this.state.brand.image" $class="\${this.vertical ? 'mb-2' : 'mx-1'}" style="height:30px;width:auto;max-width:300px">
@ -35,7 +46,7 @@ globalThis.Component.register('Nav', container => {
<div $class="\${this.vertical ? 'small text-uppercase text-body-secondary fw-semibold px-2 py-1 mt-2' : 'navbar-text small text-uppercase text-body-secondary fw-semibold px-2'} \${item.class || ''}" $text="item.label"></div>
</template>
<template $if="item.type==='button'">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'w-100 text-start py-1 px-2' : ''} \${item.class || ''}" $onclick="this.click(item)">
<button $class="nav-link \${this.state.value===item.name?'active':''} \${this.vertical ? 'w-100 text-start py-1 px-2' : ''} \${item.class || ''}" $onclick="this.click(item)">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? 'text-truncate' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
</template>

View File

@ -761,16 +761,48 @@
};
container.collapsed = globalThis.NewState({});
container.state.renderedList = [];
container.addEventListener("bind", (e) => {
container.state.selectedItem = e.detail;
});
const matchString = (val, query) => {
if (query === void 0 || query === null || query === "") return true;
return String(val || "").toLowerCase().includes(String(query).toLowerCase());
};
const matchesItem = (item, filter, filterFunc) => {
if (filterFunc && !filterFunc(item)) return false;
if (filter === 0) filter = "";
if (filter !== void 0 && filter !== null && filter !== "") {
if (typeof filter === "object") {
for (const [key, qVal] of Object.entries(filter)) {
if (qVal !== void 0 && qVal !== null && qVal !== "") {
if (!matchString(item[key], qVal)) return false;
}
}
} else {
const q = String(filter).trim();
if (q) {
const labelMatches = matchString(item[container.labelfield], q);
const summaryMatches = matchString(item[container.summaryfield], q);
if (!labelMatches && !summaryMatches) return false;
}
}
}
return true;
};
const updateFlatList = () => {
var _a;
globalThis.Util.updateDefaults(container, defaultSets);
let list = container.state.list || [];
const filter = container.state.filter;
const order = container.state.order;
if (order) {
if (typeof order === "function") {
list = [...list].sort(order);
} else if (typeof order === "string") {
let filter = container.state.filter;
let order = container.state.order;
const filterFunc = container._filterFunc;
const orderFunc = container._orderFunc;
if (filter === 0) filter = "";
if (order === 0) order = "";
if (orderFunc) {
list = [...list].sort(orderFunc);
} else if (order) {
if (typeof order === "string") {
const parts = order.trim().split(/\s+/);
const field = parts[0];
const isDesc = ((_a = parts[1]) == null ? void 0 : _a.toLowerCase()) === "desc" || field.startsWith("-");
@ -789,21 +821,7 @@
}
const flatList = [];
if (container.mode === "group") {
let filteredList = list;
if (filter) {
if (typeof filter === "function") {
filteredList = list.filter(filter);
} else if (typeof filter === "string") {
const q = filter.trim().toLowerCase();
if (q) {
filteredList = list.filter((item) => {
const label = String(item[container.labelfield] || "").toLowerCase();
const summary = String(item[container.summaryfield] || "").toLowerCase();
return label.includes(q) || summary.includes(q);
});
}
}
}
const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
const itemMap = {};
filteredList.forEach((item) => {
var _a2;
@ -811,11 +829,26 @@
});
(container.state.groups || []).forEach((group) => {
const items = itemMap[group[container.groupidfield]];
const groupLabel = String(group[container.grouplabelfield] || "").toLowerCase();
const groupSummary = String(group[container.groupsummaryfield] || "").toLowerCase();
const q = typeof filter === "string" ? filter.trim().toLowerCase() : "";
const groupMatches = q ? groupLabel.includes(q) || groupSummary.includes(q) : false;
if (items && items.length > 0 || groupMatches || !filter) {
let groupMatches = false;
if (filter !== void 0 && filter !== null && filter !== "") {
if (typeof filter === "object") {
groupMatches = true;
for (const [key, qVal] of Object.entries(filter)) {
if (qVal !== void 0 && qVal !== null && qVal !== "") {
if (!matchString(group[key], qVal)) {
groupMatches = false;
break;
}
}
}
} else {
const q = String(filter).trim();
if (q) {
groupMatches = matchString(group[container.grouplabelfield], q) || matchString(group[container.groupsummaryfield], q);
}
}
}
if (items && items.length > 0 || groupMatches || !filter && !filterFunc) {
flatList.push({ type: "group", ...group });
if (items) items.forEach((item) => flatList.push({ type: "item", ...item }));
}
@ -827,27 +860,12 @@
});
const matches = /* @__PURE__ */ new Set();
list.forEach((item) => {
let match = false;
if (!filter) {
match = true;
} else if (typeof filter === "function") {
match = filter(item);
} else if (typeof filter === "string") {
const q = filter.trim().toLowerCase();
if (!q) {
match = true;
} else {
const label = String(item[container.labelfield] || "").toLowerCase();
const summary = String(item[container.summaryfield] || "").toLowerCase();
match = label.includes(q) || summary.includes(q);
}
}
if (match) {
if (matchesItem(item, filter, filterFunc)) {
matches.add(item[container.idfield]);
}
});
const keep = new Set(matches);
if (filter) {
if (filter || filterFunc) {
matches.forEach((id) => {
let curr = itemMap[id];
while (curr) {
@ -861,7 +879,7 @@
}
const childrenMap = {};
list.forEach((item) => {
if (!filter || keep.has(item[container.idfield])) {
if (!filter && !filterFunc || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || "";
(childrenMap[parentId] ?? (childrenMap[parentId] = [])).push(item);
}
@ -876,21 +894,7 @@
});
traverse(childrenMap[""] || [], 0, []);
} else {
let filteredList = list;
if (filter) {
if (typeof filter === "function") {
filteredList = list.filter(filter);
} else if (typeof filter === "string") {
const q = filter.trim().toLowerCase();
if (q) {
filteredList = list.filter((item) => {
const label = String(item[container.labelfield] || "").toLowerCase();
const summary = String(item[container.summaryfield] || "").toLowerCase();
return label.includes(q) || summary.includes(q);
});
}
}
}
const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
filteredList.forEach((item) => flatList.push({ type: "item", ...item }));
}
container.state.flatList = flatList;
@ -932,7 +936,10 @@
} else container.state.renderedList = flatList;
});
container.selectItem = (item, index2) => {
if (container.hasAttribute("auto-select")) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield];
if (container.hasAttribute("auto-select")) {
container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield];
}
container.dispatchEvent(new CustomEvent("change", { detail: container.state.selectedItem, bubbles: false }));
container.dispatchEvent(new CustomEvent("itemclick", { bubbles: false, detail: { item, index: index2 + (container.fast ? container.state.listStartIndex || 0 : 0) } }));
};
container.selectGroup = (item, index2) => {
@ -945,31 +952,20 @@
updateFlatList();
}
};
Object.defineProperty(container, "filter", {
get: () => container.state.filter,
Object.defineProperty(container, "filterFunc", {
get: () => container._filterFunc,
set: (v) => {
container.state.filter = v;
container._filterFunc = v;
updateFlatList();
}
});
Object.defineProperty(container, "order", {
get: () => container.state.order,
Object.defineProperty(container, "orderFunc", {
get: () => container._orderFunc,
set: (v) => {
container.state.order = v;
container._orderFunc = v;
updateFlatList();
}
});
const observer = new MutationObserver(() => {
const filterAttr = container.getAttribute("filter");
if (typeof container.state.filter !== "function" && filterAttr !== container.state.filter) {
container.state.filter = filterAttr || void 0;
}
const orderAttr = container.getAttribute("order");
if (typeof container.state.order !== "function" && orderAttr !== container.state.order) {
container.state.order = orderAttr || void 0;
}
});
observer.observe(container, { attributes: true, attributeFilter: ["filter", "order"] });
container.state.filter = container.getAttribute("filter") || void 0;
container.state.order = container.getAttribute("order") || void 0;
updateFlatList();
}, globalThis.Util.makeDom(
/*html*/
@ -1001,7 +997,7 @@
<span $class="bi bi-\${this.itemicon} text-body"></span>
</template>
<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div>
<div class="text-muted small flex-fill text-end" $text="\${item[this.summaryfield]}"></div>
<div class="text-muted fs-7 small ms-2 flex-fill text-truncate text-end" $text="\${item[this.summaryfield]}"></div>
<div slot-id="item-actions"></div>
</template>
</div>
@ -1013,11 +1009,20 @@
globalThis.Component.register("Nav", (container) => {
container.vertical = container.hasAttribute("vertical");
container.state.openName = container.state.openName || null;
container.state.value = container.state.value || null;
container.addEventListener("bind", (e) => {
container.state.value = e.detail;
});
container.click = (item, noselect) => {
if (!item.noselect && !noselect) globalThis.Hash.nav = item.name;
if (!item.noselect && !noselect) {
container.state.value = item.name;
container.dispatchEvent(new CustomEvent("change", { detail: item.name, bubbles: false }));
}
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
};
container.clickSubitem = (item) => {
container.state.value = item.name;
container.dispatchEvent(new CustomEvent("change", { detail: item.name, bubbles: false }));
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
};
container.toggleGroup = (item) => {
@ -1027,7 +1032,7 @@
}, globalThis.Util.makeDom(
/*html*/
`
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100 align-self-stretch overflow-visible' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-2 \${this.vertical ? 'py-3' : 'py-0'} align-items-center \${this.vertical ? 'align-items-start' : ''}" $style="this.vertical ? 'min-height:0;' : ''">
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100 align-self-stretch overflow-visible' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-2 \${this.vertical ? 'py-3' : 'py-1'} align-items-center \${this.vertical ? 'align-items-start' : ''}" $style="this.vertical ? 'min-height:0;' : ''">
<div class="d-flex align-items-center gap-2">
<template $if="this.state?.brand?.image">
<img $src="this.state.brand.image" $class="\${this.vertical ? 'mb-2' : 'mx-1'}" style="height:30px;width:auto;max-width:300px">
@ -1046,7 +1051,7 @@
<div $class="\${this.vertical ? 'small text-uppercase text-body-secondary fw-semibold px-2 py-1 mt-2' : 'navbar-text small text-uppercase text-body-secondary fw-semibold px-2'} \${item.class || ''}" $text="item.label"></div>
</template>
<template $if="item.type==='button'">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'w-100 text-start py-1 px-2' : ''} \${item.class || ''}" $onclick="this.click(item)">
<button $class="nav-link \${this.state.value===item.name?'active':''} \${this.vertical ? 'w-100 text-start py-1 px-2' : ''} \${item.class || ''}" $onclick="this.click(item)">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? 'text-truncate' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
</template>

View File

@ -222,12 +222,26 @@
if (attr.name === "style") {
if (to.hasAttribute("style")) to.setAttribute("style", `${attr.value}; ${to.getAttribute("style")}`);
else to.setAttribute("style", attr.value);
} else if (!to.hasAttribute(attr.name)) {
} else if (to.hasAttribute(attr.name)) {
const isClass = ["$class", "st-class"].includes(attr.name);
const isStyle = ["$style", "st-style"].includes(attr.name);
if (isClass || isStyle) {
const oldVal = to.getAttribute(attr.name);
const newVal = attr.value;
const delimiter = isClass ? " " : "; ";
const oldExpr = oldVal.includes("${") ? oldVal : `\${${oldVal}}`;
const newExpr = newVal.includes("${") ? newVal : `\${${newVal}}`;
to.setAttribute(attr.name, `${newExpr}${delimiter}${oldExpr}`);
}
} else {
to.setAttribute(attr.name, attr.value);
}
});
}
const toClassList = [...to.classList];
to.className = "";
to.classList.add(...from.classList);
to.classList.add(...toClassList);
const target = to.tagName === "TEMPLATE" ? to.content : to;
const sourceNodes = from.tagName === "TEMPLATE" ? from.content.childNodes : from.childNodes;
Array.from(sourceNodes).forEach((child) => target.appendChild(child));
@ -318,6 +332,7 @@
}
}
_setActiveBinding(null);
binding.lastResult = result;
if (binding.prop) {
const prop = binding.prop;
let o = node;
@ -441,7 +456,21 @@
if (attr === "text") node.textContent = result ?? "";
else if (attr === "html") node.innerHTML = result ?? "";
else if (node.tagName === "IMG" && attr === "src" && result.includes(".svg")) node.setAttribute("_src", result ?? "");
else node.setAttribute(attr, result ?? "");
else if (attr === "class") {
if (node._staticClasses === void 0) {
node._staticClasses = node.getAttribute("class") || "";
}
const staticClasses = node._staticClasses;
const finalClass = result ? staticClasses ? `${result} ${staticClasses}` : result : staticClasses;
node.setAttribute("class", finalClass.trim().replace(/\s+/g, " "));
} else if (attr === "style") {
if (node._staticStyles === void 0) {
node._staticStyles = node.getAttribute("style") || "";
}
const staticStyles = node._staticStyles;
const finalStyle = result ? staticStyles ? `${result}; ${staticStyles}` : result : staticStyles;
node.setAttribute("style", finalStyle.trim().replace(/;;+/g, ";").replace(/^;+\s*|;\s*$/g, ""));
} else node.setAttribute(attr, result ?? "");
}
}
}
@ -472,7 +501,6 @@
if (prop[i]) o = o[_a2 = prop[i]] ?? (o[_a2] = {});
}
o[prop[prop.length - 1]] = result;
node.removeAttribute(attr.name);
}
});
_makeComponent(node.tagName, node, scanObj);
@ -486,9 +514,9 @@
}
let attrs = [];
if (node.tagName === "TEMPLATE") {
["$if", "$each", "st-if", "st-each"].forEach((n) => node.hasAttribute(n) && attrs.push(node.getAttributeNode(n)));
["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].forEach((n) => node.hasAttribute(n) && attrs.push(node.getAttributeNode(n)));
} else {
attrs = Array.from(node.attributes).filter((a) => (a.name.startsWith("$") || a.name.startsWith("st-")) && !["$if", "$each", "st-if", "st-each"].includes(a.name) || a.name.includes("."));
attrs = Array.from(node.attributes).filter((a) => (a.name.startsWith("$") || a.name.startsWith("st-")) && !["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].includes(a.name) || a.name.includes("."));
}
if (node._thisObj && scanObj.thisObj && node._thisObj !== scanObj.thisObj) node._thisObj.parent = scanObj.thisObj;
if (!node._thisObj) node._thisObj = scanObj.thisObj || null;
@ -552,9 +580,9 @@
});
node._stTranslated = true;
}
if (node.tagName !== "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("$each") || node.hasAttribute("st-if") || node.hasAttribute("st-each"))) {
if (node.tagName !== "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("$each") || node.hasAttribute("st-if") || node.hasAttribute("st-each") || node.hasAttribute("$$if") || node.hasAttribute("$$each") || node.hasAttribute("st-st-if") || node.hasAttribute("st-st-each"))) {
const template = document.createElement("TEMPLATE");
const attrs = Array.from(node.attributes).filter((attr) => ["$if", "$each", "st-if", "st-each"].includes(attr.name) || (node.hasAttribute("$each") || node.hasAttribute("st-each")) && ["as", "index"].includes(attr.name));
const attrs = Array.from(node.attributes).filter((attr) => ["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].includes(attr.name) || (node.hasAttribute("$each") || node.hasAttribute("st-each") || node.hasAttribute("$$each") || node.hasAttribute("st-st-each")) && ["as", "index"].includes(attr.name));
attrs.forEach((attr) => {
template.setAttribute(attr.name, attr.value);
node.removeAttribute(attr.name);
@ -565,13 +593,13 @@
_scanTree(template, scanObj);
return;
}
if (node.tagName === "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("st-if")) && (node.hasAttribute("$each") || node.hasAttribute("st-each"))) {
if (node.tagName === "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("st-if") || node.hasAttribute("$$if") || node.hasAttribute("st-st-if")) && (node.hasAttribute("$each") || node.hasAttribute("st-each") || node.hasAttribute("$$each") || node.hasAttribute("st-st-each"))) {
const template = document.createElement("TEMPLATE");
const attrs = Array.from(node.attributes).filter((attr2) => ["$if", "$each", "st-if", "st-each"].includes(attr2.name));
const attrs = Array.from(node.attributes).filter((attr2) => ["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].includes(attr2.name));
const attr = attrs[attrs.length - 1];
template.setAttribute(attr.name, attr.value);
node.removeAttribute(attr.name);
if (attr.name === "$each" || attr.name === "st-each") {
if (["$each", "st-each", "$$each", "st-st-each"].includes(attr.name)) {
Array.from(node.attributes).filter((attr2) => ["as", "index"].includes(attr2.name)).forEach((attr2) => {
template.setAttribute(attr2.name, attr2.value);
node.removeAttribute(attr2.name);
@ -658,5 +686,9 @@
exports2.State = State;
exports2.Util = Util;
exports2.__unsafeRefreshState = __unsafeRefreshState;
exports2._returnCode = _returnCode;
exports2._runCode = _runCode;
exports2.onNotifyUpdate = _onNotifyUpdate;
exports2.setActiveBinding = _setActiveBinding;
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
});

View File

@ -38,6 +38,7 @@ test('List fixtures cover normal, grouped, tree and virtual modes', async ({ pag
});
test('List component supports internal filtering and sorting', async ({ page }) => {
page.on('console', msg => console.log('BROWSER CONSOLE:', msg.text()));
await page.goto('/test/list_test.html');
// 1. Test Standard List Filtering

View File

@ -16,32 +16,32 @@
<body>
<div class="test-container">
<div class="list-card">
<h5 class="d-flex align-items-center justify-content-between">
<List id="listStd" class="flex-fill" $.state.list="State.stdItems" auto-select></List>
<h5 class="d-flex align-items-center justify-content-between order-first">
<span>1. Standard List (Normal)</span>
<div class="d-flex gap-2">
<input type="text" id="searchInput" class="form-control form-control-sm" style="width: 120px;" placeholder="Search..." $bind="State.stdFilter">
<select id="sortSelect" class="form-select form-select-sm" style="width: 100px;" $bind="State.stdOrder">
<input type="text" id="searchInput" class="form-control form-control-sm" style="width: 120px;" placeholder="Search..." $bind="listStd.state.filter">
<select id="sortSelect" class="form-select form-select-sm" style="width: 100px;" $bind="listStd.state.order">
<option value="">No Sort</option>
<option value="label">Label Asc</option>
<option value="-label">Label Desc</option>
</select>
</div>
</h5>
<List id="listStd" class="flex-fill" $.state.list="State.stdItems" $filter="State.stdFilter || ''" $order="State.stdOrder" auto-select></List>
</div>
<div class="list-card">
<h5 class="d-flex align-items-center justify-content-between">
<List id="listGrp" mode="group" class="flex-fill" $.state.list="State.grpItems" $.state.groups="State.groups"></List>
<h5 class="d-flex align-items-center justify-content-between order-first">
<span>2. Group List (Mode: Group)</span>
<input type="text" id="grpSearchInput" class="form-control form-control-sm" style="width: 120px;" placeholder="Search..." $bind="State.grpFilter">
<input type="text" id="grpSearchInput" class="form-control form-control-sm" style="width: 120px;" placeholder="Search..." $bind="listGrp.state.filter">
</h5>
<List id="listGrp" mode="group" class="flex-fill" $.state.list="State.grpItems" $.state.groups="State.groups" $filter="State.grpFilter || ''"></List>
</div>
<div class="list-card">
<h5 class="d-flex align-items-center justify-content-between">
<List id="listTree" mode="tree" collapsible class="flex-fill" $.state.list="State.treeItems"></List>
<h5 class="d-flex align-items-center justify-content-between order-first">
<span>3. Tree List (Mode: Tree + Collapsible)</span>
<input type="text" id="treeSearchInput" class="form-control form-control-sm" style="width: 120px;" placeholder="Search..." $bind="State.treeFilter">
<input type="text" id="treeSearchInput" class="form-control form-control-sm" style="width: 120px;" placeholder="Search..." $bind="listTree.state.filter">
</h5>
<List id="listTree" mode="tree" collapsible class="flex-fill" $.state.list="State.treeItems" $filter="State.treeFilter || ''"></List>
</div>
<div class="list-card">
<h5>4. FAST Virtual List (10,000 Items + Dynamic Height)</h5>
@ -105,7 +105,6 @@
summary: lorem[i % 4]
});
}
State.stdItems = stdItems;
State.groups = groups;
State.grpItems = grpItems;