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

1079
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.collapsed = globalThis.NewState({});
container.state.renderedList = []; 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 = () => { const updateFlatList = () => {
var _a; var _a;
globalThis.Util.updateDefaults(container, defaultSets); globalThis.Util.updateDefaults(container, defaultSets);
let list = container.state.list || []; let list = container.state.list || [];
const filter = container.state.filter; let filter = container.state.filter;
const order = container.state.order; let order = container.state.order;
if (order) { const filterFunc = container._filterFunc;
if (typeof order === "function") { const orderFunc = container._orderFunc;
list = [...list].sort(order); if (filter === 0) filter = "";
} else if (typeof order === "string") { if (order === 0) order = "";
if (orderFunc) {
list = [...list].sort(orderFunc);
} else if (order) {
if (typeof order === "string") {
const parts = order.trim().split(/\s+/); const parts = order.trim().split(/\s+/);
const field = parts[0]; const field = parts[0];
const isDesc = ((_a = parts[1]) == null ? void 0 : _a.toLowerCase()) === "desc" || field.startsWith("-"); const isDesc = ((_a = parts[1]) == null ? void 0 : _a.toLowerCase()) === "desc" || field.startsWith("-");
@ -789,21 +821,7 @@
} }
const flatList = []; const flatList = [];
if (container.mode === "group") { if (container.mode === "group") {
let filteredList = list; const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
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 = {}; const itemMap = {};
filteredList.forEach((item) => { filteredList.forEach((item) => {
var _a2; var _a2;
@ -811,11 +829,26 @@
}); });
(container.state.groups || []).forEach((group) => { (container.state.groups || []).forEach((group) => {
const items = itemMap[group[container.groupidfield]]; const items = itemMap[group[container.groupidfield]];
const groupLabel = String(group[container.grouplabelfield] || "").toLowerCase(); let groupMatches = false;
const groupSummary = String(group[container.groupsummaryfield] || "").toLowerCase(); if (filter !== void 0 && filter !== null && filter !== "") {
const q = typeof filter === "string" ? filter.trim().toLowerCase() : ""; if (typeof filter === "object") {
const groupMatches = q ? groupLabel.includes(q) || groupSummary.includes(q) : false; groupMatches = true;
if (items && items.length > 0 || groupMatches || !filter) { 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 }); flatList.push({ type: "group", ...group });
if (items) items.forEach((item) => flatList.push({ type: "item", ...item })); if (items) items.forEach((item) => flatList.push({ type: "item", ...item }));
} }
@ -827,27 +860,12 @@
}); });
const matches = /* @__PURE__ */ new Set(); const matches = /* @__PURE__ */ new Set();
list.forEach((item) => { list.forEach((item) => {
let match = false; if (matchesItem(item, filter, filterFunc)) {
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]); matches.add(item[container.idfield]);
} }
}); });
const keep = new Set(matches); const keep = new Set(matches);
if (filter) { if (filter || filterFunc) {
matches.forEach((id) => { matches.forEach((id) => {
let curr = itemMap[id]; let curr = itemMap[id];
while (curr) { while (curr) {
@ -861,7 +879,7 @@
} }
const childrenMap = {}; const childrenMap = {};
list.forEach((item) => { list.forEach((item) => {
if (!filter || keep.has(item[container.idfield])) { if (!filter && !filterFunc || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || ""; const parentId = item[container.parentfield] || "";
(childrenMap[parentId] ?? (childrenMap[parentId] = [])).push(item); (childrenMap[parentId] ?? (childrenMap[parentId] = [])).push(item);
} }
@ -876,21 +894,7 @@
}); });
traverse(childrenMap[""] || [], 0, []); traverse(childrenMap[""] || [], 0, []);
} else { } else {
let filteredList = list; const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
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 })); filteredList.forEach((item) => flatList.push({ type: "item", ...item }));
} }
container.state.flatList = flatList; container.state.flatList = flatList;
@ -932,7 +936,10 @@
} else container.state.renderedList = flatList; } else container.state.renderedList = flatList;
}); });
container.selectItem = (item, index2) => { 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.dispatchEvent(new CustomEvent("itemclick", { bubbles: false, detail: { item, index: index2 + (container.fast ? container.state.listStartIndex || 0 : 0) } }));
}; };
container.selectGroup = (item, index2) => { container.selectGroup = (item, index2) => {
@ -945,31 +952,20 @@
updateFlatList(); updateFlatList();
} }
}; };
Object.defineProperty(container, "filter", { Object.defineProperty(container, "filterFunc", {
get: () => container.state.filter, get: () => container._filterFunc,
set: (v) => { set: (v) => {
container.state.filter = v; container._filterFunc = v;
updateFlatList();
} }
}); });
Object.defineProperty(container, "order", { Object.defineProperty(container, "orderFunc", {
get: () => container.state.order, get: () => container._orderFunc,
set: (v) => { 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(); updateFlatList();
}, globalThis.Util.makeDom( }, globalThis.Util.makeDom(
/*html*/ /*html*/
@ -1001,7 +997,7 @@
<span $class="bi bi-\${this.itemicon} text-body"></span> <span $class="bi bi-\${this.itemicon} text-body"></span>
</template> </template>
<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div> <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> <div slot-id="item-actions"></div>
</template> </template>
</div> </div>
@ -1013,11 +1009,20 @@
globalThis.Component.register("Nav", (container) => { globalThis.Component.register("Nav", (container) => {
container.vertical = container.hasAttribute("vertical"); container.vertical = container.hasAttribute("vertical");
container.state.openName = container.state.openName || null; 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) => { 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.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
}; };
container.clickSubitem = (item) => { 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.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
}; };
container.toggleGroup = (item) => { container.toggleGroup = (item) => {
@ -1027,7 +1032,7 @@
}, globalThis.Util.makeDom( }, globalThis.Util.makeDom(
/*html*/ /*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"> <div class="d-flex align-items-center gap-2">
<template $if="this.state?.brand?.image"> <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"> <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> <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>
<template $if="item.type==='button'"> <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> <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> </button>
</template> </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", "name": "@apigo.cc/base",
"version": "1.0.22", "version": "1.0.23",
"type": "module", "type": "module",
"main": "dist/base.js", "main": "dist/base.js",
"files": [ "files": [

View File

@ -17,16 +17,53 @@ globalThis.Component.register('List', container => {
container.collapsed = globalThis.NewState({}) container.collapsed = globalThis.NewState({})
container.state.renderedList = [] container.state.renderedList = []
container.addEventListener('bind', e => {
container.state.selectedItem = e.detail
})
const matchString = (val, query) => {
if (query === undefined || 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 !== 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 updateFlatList = () => { const updateFlatList = () => {
globalThis.Util.updateDefaults(container, defaultSets) globalThis.Util.updateDefaults(container, defaultSets)
let list = container.state.list || [] let list = container.state.list || []
const filter = container.state.filter let filter = container.state.filter
const order = container.state.order let order = container.state.order
const filterFunc = container._filterFunc
const orderFunc = container._orderFunc
if (order) { if (filter === 0) filter = ''
if (typeof order === 'function') { if (order === 0) order = ''
list = [...list].sort(order)
} else if (typeof order === 'string') { if (orderFunc) {
list = [...list].sort(orderFunc)
} else if (order) {
if (typeof order === 'string') {
const parts = order.trim().split(/\s+/) const parts = order.trim().split(/\s+/)
const field = parts[0] const field = parts[0]
const isDesc = parts[1]?.toLowerCase() === 'desc' || field.startsWith('-') const isDesc = parts[1]?.toLowerCase() === 'desc' || field.startsWith('-')
@ -46,31 +83,33 @@ globalThis.Component.register('List', container => {
const flatList = [] const flatList = []
if (container.mode === 'group') { if (container.mode === 'group') {
let filteredList = list const filteredList = list.filter(item => matchesItem(item, filter, filterFunc))
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 = {} const itemMap = {}
filteredList.forEach(item => (itemMap[item[container.groupfield]] ??= []).push(item)); filteredList.forEach(item => (itemMap[item[container.groupfield]] ??= []).push(item));
(container.state.groups || []).forEach(group => { (container.state.groups || []).forEach(group => {
const items = itemMap[group[container.groupidfield]] const items = itemMap[group[container.groupidfield]]
const groupLabel = String(group[container.grouplabelfield] || '').toLowerCase() let groupMatches = false
const groupSummary = String(group[container.groupsummaryfield] || '').toLowerCase() if (filter !== undefined && filter !== null && filter !== '') {
const q = (typeof filter === 'string') ? filter.trim().toLowerCase() : '' if (typeof filter === 'object') {
const groupMatches = q ? (groupLabel.includes(q) || groupSummary.includes(q)) : false 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)
}
}
}
if ((items && items.length > 0) || groupMatches || !filter) { if ((items && items.length > 0) || groupMatches || (!filter && !filterFunc)) {
flatList.push({ type: 'group', ...group }) flatList.push({ type: 'group', ...group })
if (items) items.forEach(item => flatList.push({ type: 'item', ...item })) if (items) items.forEach(item => flatList.push({ type: 'item', ...item }))
} }
@ -81,28 +120,13 @@ globalThis.Component.register('List', container => {
const matches = new Set() const matches = new Set()
list.forEach(item => { list.forEach(item => {
let match = false if (matchesItem(item, filter, filterFunc)) {
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]) matches.add(item[container.idfield])
} }
}) })
const keep = new Set(matches) const keep = new Set(matches)
if (filter) { if (filter || filterFunc) {
matches.forEach(id => { matches.forEach(id => {
let curr = itemMap[id] let curr = itemMap[id]
while (curr) { while (curr) {
@ -117,9 +141,9 @@ globalThis.Component.register('List', container => {
const childrenMap = {} const childrenMap = {}
list.forEach(item => { list.forEach(item => {
if (!filter || keep.has(item[container.idfield])) { if ((!filter && !filterFunc) || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || '' const parentId = item[container.parentfield] || ''
;(childrenMap[parentId] ??= []).push(item) ; (childrenMap[parentId] ??= []).push(item)
} }
}) })
@ -132,21 +156,7 @@ globalThis.Component.register('List', container => {
}) })
traverse(childrenMap[''] || [], 0, []) traverse(childrenMap[''] || [], 0, [])
} else { } else {
let filteredList = list const filteredList = list.filter(item => matchesItem(item, filter, filterFunc))
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 })) filteredList.forEach(item => flatList.push({ type: 'item', ...item }))
} }
container.state.flatList = flatList container.state.flatList = flatList
@ -187,7 +197,10 @@ globalThis.Component.register('List', container => {
}) })
container.selectItem = (item, index) => { container.selectItem = (item, index) => {
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: index + (container.fast ? (container.state.listStartIndex || 0) : 0) } })) container.dispatchEvent(new CustomEvent('itemclick', { bubbles: false, detail: { item, index: index + (container.fast ? (container.state.listStartIndex || 0) : 0) } }))
} }
container.selectGroup = (item, index) => { container.selectGroup = (item, index) => {
@ -196,30 +209,15 @@ globalThis.Component.register('List', container => {
} }
container.toggleCollapse = (item) => { if (container.collapsible && item._hasChildren) { container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; updateFlatList(); } } container.toggleCollapse = (item) => { if (container.collapsible && item._hasChildren) { container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; updateFlatList(); } }
Object.defineProperty(container, 'filter', { Object.defineProperty(container, 'filterFunc', {
get: () => container.state.filter, get: () => container._filterFunc,
set: v => { container.state.filter = v; } set: v => { container._filterFunc = v; updateFlatList(); }
}) })
Object.defineProperty(container, 'order', { Object.defineProperty(container, 'orderFunc', {
get: () => container.state.order, get: () => container._orderFunc,
set: v => { container.state.order = v; } set: 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 || 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'] })
container.state.filter = container.getAttribute('filter') || undefined
container.state.order = container.getAttribute('order') || undefined
updateFlatList() updateFlatList()
}, globalThis.Util.makeDom(/*html*/` }, globalThis.Util.makeDom(/*html*/`
<div class="list-group overflow-auto" onscroll="this.refresh()" style="overflow-anchor:none"> <div class="list-group overflow-auto" onscroll="this.refresh()" style="overflow-anchor:none">
@ -249,7 +247,7 @@ globalThis.Component.register('List', container => {
<span $class="bi bi-\${this.itemicon} text-body"></span> <span $class="bi bi-\${this.itemicon} text-body"></span>
</template> </template>
<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div> <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> <div slot-id="item-actions"></div>
</template> </template>
</div> </div>

View File

@ -4,11 +4,22 @@
globalThis.Component.register('Nav', container => { globalThis.Component.register('Nav', container => {
container.vertical = container.hasAttribute('vertical') container.vertical = container.hasAttribute('vertical')
container.state.openName = container.state.openName || null 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) => { 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.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false }))
} }
container.clickSubitem = item => { 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.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false }))
} }
container.toggleGroup = item => { 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 })) container.dispatchEvent(new CustomEvent('nav', { detail: { item, open: container.state.openName === item.name }, bubbles: false }))
} }
}, globalThis.Util.makeDom(/*html*/` }, 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"> <div class="d-flex align-items-center gap-2">
<template $if="this.state?.brand?.image"> <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"> <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> <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>
<template $if="item.type==='button'"> <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> <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> </button>
</template> </template>

View File

@ -761,16 +761,48 @@
}; };
container.collapsed = globalThis.NewState({}); container.collapsed = globalThis.NewState({});
container.state.renderedList = []; 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 = () => { const updateFlatList = () => {
var _a; var _a;
globalThis.Util.updateDefaults(container, defaultSets); globalThis.Util.updateDefaults(container, defaultSets);
let list = container.state.list || []; let list = container.state.list || [];
const filter = container.state.filter; let filter = container.state.filter;
const order = container.state.order; let order = container.state.order;
if (order) { const filterFunc = container._filterFunc;
if (typeof order === "function") { const orderFunc = container._orderFunc;
list = [...list].sort(order); if (filter === 0) filter = "";
} else if (typeof order === "string") { if (order === 0) order = "";
if (orderFunc) {
list = [...list].sort(orderFunc);
} else if (order) {
if (typeof order === "string") {
const parts = order.trim().split(/\s+/); const parts = order.trim().split(/\s+/);
const field = parts[0]; const field = parts[0];
const isDesc = ((_a = parts[1]) == null ? void 0 : _a.toLowerCase()) === "desc" || field.startsWith("-"); const isDesc = ((_a = parts[1]) == null ? void 0 : _a.toLowerCase()) === "desc" || field.startsWith("-");
@ -789,21 +821,7 @@
} }
const flatList = []; const flatList = [];
if (container.mode === "group") { if (container.mode === "group") {
let filteredList = list; const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
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 = {}; const itemMap = {};
filteredList.forEach((item) => { filteredList.forEach((item) => {
var _a2; var _a2;
@ -811,11 +829,26 @@
}); });
(container.state.groups || []).forEach((group) => { (container.state.groups || []).forEach((group) => {
const items = itemMap[group[container.groupidfield]]; const items = itemMap[group[container.groupidfield]];
const groupLabel = String(group[container.grouplabelfield] || "").toLowerCase(); let groupMatches = false;
const groupSummary = String(group[container.groupsummaryfield] || "").toLowerCase(); if (filter !== void 0 && filter !== null && filter !== "") {
const q = typeof filter === "string" ? filter.trim().toLowerCase() : ""; if (typeof filter === "object") {
const groupMatches = q ? groupLabel.includes(q) || groupSummary.includes(q) : false; groupMatches = true;
if (items && items.length > 0 || groupMatches || !filter) { 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 }); flatList.push({ type: "group", ...group });
if (items) items.forEach((item) => flatList.push({ type: "item", ...item })); if (items) items.forEach((item) => flatList.push({ type: "item", ...item }));
} }
@ -827,27 +860,12 @@
}); });
const matches = /* @__PURE__ */ new Set(); const matches = /* @__PURE__ */ new Set();
list.forEach((item) => { list.forEach((item) => {
let match = false; if (matchesItem(item, filter, filterFunc)) {
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]); matches.add(item[container.idfield]);
} }
}); });
const keep = new Set(matches); const keep = new Set(matches);
if (filter) { if (filter || filterFunc) {
matches.forEach((id) => { matches.forEach((id) => {
let curr = itemMap[id]; let curr = itemMap[id];
while (curr) { while (curr) {
@ -861,7 +879,7 @@
} }
const childrenMap = {}; const childrenMap = {};
list.forEach((item) => { list.forEach((item) => {
if (!filter || keep.has(item[container.idfield])) { if (!filter && !filterFunc || keep.has(item[container.idfield])) {
const parentId = item[container.parentfield] || ""; const parentId = item[container.parentfield] || "";
(childrenMap[parentId] ?? (childrenMap[parentId] = [])).push(item); (childrenMap[parentId] ?? (childrenMap[parentId] = [])).push(item);
} }
@ -876,21 +894,7 @@
}); });
traverse(childrenMap[""] || [], 0, []); traverse(childrenMap[""] || [], 0, []);
} else { } else {
let filteredList = list; const filteredList = list.filter((item) => matchesItem(item, filter, filterFunc));
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 })); filteredList.forEach((item) => flatList.push({ type: "item", ...item }));
} }
container.state.flatList = flatList; container.state.flatList = flatList;
@ -932,7 +936,10 @@
} else container.state.renderedList = flatList; } else container.state.renderedList = flatList;
}); });
container.selectItem = (item, index2) => { 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.dispatchEvent(new CustomEvent("itemclick", { bubbles: false, detail: { item, index: index2 + (container.fast ? container.state.listStartIndex || 0 : 0) } }));
}; };
container.selectGroup = (item, index2) => { container.selectGroup = (item, index2) => {
@ -945,31 +952,20 @@
updateFlatList(); updateFlatList();
} }
}; };
Object.defineProperty(container, "filter", { Object.defineProperty(container, "filterFunc", {
get: () => container.state.filter, get: () => container._filterFunc,
set: (v) => { set: (v) => {
container.state.filter = v; container._filterFunc = v;
updateFlatList();
} }
}); });
Object.defineProperty(container, "order", { Object.defineProperty(container, "orderFunc", {
get: () => container.state.order, get: () => container._orderFunc,
set: (v) => { 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(); updateFlatList();
}, globalThis.Util.makeDom( }, globalThis.Util.makeDom(
/*html*/ /*html*/
@ -1001,7 +997,7 @@
<span $class="bi bi-\${this.itemicon} text-body"></span> <span $class="bi bi-\${this.itemicon} text-body"></span>
</template> </template>
<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div> <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> <div slot-id="item-actions"></div>
</template> </template>
</div> </div>
@ -1013,11 +1009,20 @@
globalThis.Component.register("Nav", (container) => { globalThis.Component.register("Nav", (container) => {
container.vertical = container.hasAttribute("vertical"); container.vertical = container.hasAttribute("vertical");
container.state.openName = container.state.openName || null; 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) => { 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.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
}; };
container.clickSubitem = (item) => { 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.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
}; };
container.toggleGroup = (item) => { container.toggleGroup = (item) => {
@ -1027,7 +1032,7 @@
}, globalThis.Util.makeDom( }, globalThis.Util.makeDom(
/*html*/ /*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"> <div class="d-flex align-items-center gap-2">
<template $if="this.state?.brand?.image"> <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"> <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> <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>
<template $if="item.type==='button'"> <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> <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> </button>
</template> </template>

View File

@ -222,12 +222,26 @@
if (attr.name === "style") { if (attr.name === "style") {
if (to.hasAttribute("style")) to.setAttribute("style", `${attr.value}; ${to.getAttribute("style")}`); if (to.hasAttribute("style")) to.setAttribute("style", `${attr.value}; ${to.getAttribute("style")}`);
else to.setAttribute("style", attr.value); 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); to.setAttribute(attr.name, attr.value);
} }
}); });
} }
const toClassList = [...to.classList];
to.className = "";
to.classList.add(...from.classList); to.classList.add(...from.classList);
to.classList.add(...toClassList);
const target = to.tagName === "TEMPLATE" ? to.content : to; const target = to.tagName === "TEMPLATE" ? to.content : to;
const sourceNodes = from.tagName === "TEMPLATE" ? from.content.childNodes : from.childNodes; const sourceNodes = from.tagName === "TEMPLATE" ? from.content.childNodes : from.childNodes;
Array.from(sourceNodes).forEach((child) => target.appendChild(child)); Array.from(sourceNodes).forEach((child) => target.appendChild(child));
@ -318,6 +332,7 @@
} }
} }
_setActiveBinding(null); _setActiveBinding(null);
binding.lastResult = result;
if (binding.prop) { if (binding.prop) {
const prop = binding.prop; const prop = binding.prop;
let o = node; let o = node;
@ -441,7 +456,21 @@
if (attr === "text") node.textContent = result ?? ""; if (attr === "text") node.textContent = result ?? "";
else if (attr === "html") node.innerHTML = result ?? ""; else if (attr === "html") node.innerHTML = result ?? "";
else if (node.tagName === "IMG" && attr === "src" && result.includes(".svg")) node.setAttribute("_src", 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] = {}); if (prop[i]) o = o[_a2 = prop[i]] ?? (o[_a2] = {});
} }
o[prop[prop.length - 1]] = result; o[prop[prop.length - 1]] = result;
node.removeAttribute(attr.name);
} }
}); });
_makeComponent(node.tagName, node, scanObj); _makeComponent(node.tagName, node, scanObj);
@ -486,9 +514,9 @@
} }
let attrs = []; let attrs = [];
if (node.tagName === "TEMPLATE") { 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 { } 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 && scanObj.thisObj && node._thisObj !== scanObj.thisObj) node._thisObj.parent = scanObj.thisObj;
if (!node._thisObj) node._thisObj = scanObj.thisObj || null; if (!node._thisObj) node._thisObj = scanObj.thisObj || null;
@ -552,9 +580,9 @@
}); });
node._stTranslated = true; 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 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) => { attrs.forEach((attr) => {
template.setAttribute(attr.name, attr.value); template.setAttribute(attr.name, attr.value);
node.removeAttribute(attr.name); node.removeAttribute(attr.name);
@ -565,13 +593,13 @@
_scanTree(template, scanObj); _scanTree(template, scanObj);
return; 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 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]; const attr = attrs[attrs.length - 1];
template.setAttribute(attr.name, attr.value); template.setAttribute(attr.name, attr.value);
node.removeAttribute(attr.name); 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) => { Array.from(node.attributes).filter((attr2) => ["as", "index"].includes(attr2.name)).forEach((attr2) => {
template.setAttribute(attr2.name, attr2.value); template.setAttribute(attr2.name, attr2.value);
node.removeAttribute(attr2.name); node.removeAttribute(attr2.name);
@ -658,5 +686,9 @@
exports2.State = State; exports2.State = State;
exports2.Util = Util; exports2.Util = Util;
exports2.__unsafeRefreshState = __unsafeRefreshState; exports2.__unsafeRefreshState = __unsafeRefreshState;
exports2._returnCode = _returnCode;
exports2._runCode = _runCode;
exports2.onNotifyUpdate = _onNotifyUpdate;
exports2.setActiveBinding = _setActiveBinding;
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); 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 }) => { 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'); await page.goto('/test/list_test.html');
// 1. Test Standard List Filtering // 1. Test Standard List Filtering

View File

@ -16,32 +16,32 @@
<body> <body>
<div class="test-container"> <div class="test-container">
<div class="list-card"> <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> <span>1. Standard List (Normal)</span>
<div class="d-flex gap-2"> <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"> <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="State.stdOrder"> <select id="sortSelect" class="form-select form-select-sm" style="width: 100px;" $bind="listStd.state.order">
<option value="">No Sort</option> <option value="">No Sort</option>
<option value="label">Label Asc</option> <option value="label">Label Asc</option>
<option value="-label">Label Desc</option> <option value="-label">Label Desc</option>
</select> </select>
</div> </div>
</h5> </h5>
<List id="listStd" class="flex-fill" $.state.list="State.stdItems" $filter="State.stdFilter || ''" $order="State.stdOrder" auto-select></List>
</div> </div>
<div class="list-card"> <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> <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> </h5>
<List id="listGrp" mode="group" class="flex-fill" $.state.list="State.grpItems" $.state.groups="State.groups" $filter="State.grpFilter || ''"></List>
</div> </div>
<div class="list-card"> <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> <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> </h5>
<List id="listTree" mode="tree" collapsible class="flex-fill" $.state.list="State.treeItems" $filter="State.treeFilter || ''"></List>
</div> </div>
<div class="list-card"> <div class="list-card">
<h5>4. FAST Virtual List (10,000 Items + Dynamic Height)</h5> <h5>4. FAST Virtual List (10,000 Items + Dynamic Height)</h5>
@ -105,7 +105,6 @@
summary: lorem[i % 4] summary: lorem[i % 4]
}); });
} }
State.stdItems = stdItems; State.stdItems = stdItems;
State.groups = groups; State.groups = groups;
State.grpItems = grpItems; State.grpItems = grpItems;