Compare commits

...

2 Commits

Author SHA1 Message Date
AI Engineer
93f6953f4c chore(base): 同步版本号至1.0.21(by AI) 2026-06-28 00:01:22 +08:00
AI Engineer
0b8e1b2952 fix(base): 修复导航与API预设请求(by AI) 2026-06-28 00:00:16 +08:00
8 changed files with 270 additions and 88 deletions

View File

@ -1,5 +1,10 @@
# Changelog # Changelog
## [1.0.21] - 2026-06-27
### Fixed
- **API**: Preserved declarative preset fields injected through `$.request.*` when the `API` component initializes its internal reactive request state.
- **Nav**: Restored horizontal dropdown behavior by removing container-level clipping, added `label`/`class` support for menu grouping, and switched vertical mode dropdown groups to inline expansion so long sidebars can scroll without popup clipping.
## [1.0.18] - 2026-06-11 ## [1.0.18] - 2026-06-11
### Changed ### Changed
- **Component-Based Refactor**: Reorganized source code into independent, PascalCase named modules (e.g., `AutoForm.js`, `TagsInput.js`, `List.js`) to improve maintainability. - **Component-Based Refactor**: Reorganized source code into independent, PascalCase named modules (e.g., `AutoForm.js`, `TagsInput.js`, `List.js`) to improve maintainability.

View File

@ -24,9 +24,9 @@
<!-- 1. 基础状态机 (地基) --> <!-- 1. 基础状态机 (地基) -->
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.18/dist/state.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.18/dist/state.min.js"></script>
<!-- 2. Bootstrap 适配层 --> <!-- 2. Bootstrap 适配层 -->
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.7/dist/bootstrap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.9/dist/bootstrap.min.js"></script>
<!-- 3. 本业务组件库 --> <!-- 3. 本业务组件库 -->
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/base@1.0.18/dist/base.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@apigo.cc/base@1.0.21/dist/base.min.js"></script>
``` ```
<script> <script>
// 4. 数据先行 (在 body 解析前定义) // 4. 数据先行 (在 body 解析前定义)
@ -215,6 +215,7 @@ const { HTTP, UI, AutoForm, State } = window.ApigoBase
// 1. 预先确定品牌与菜单数据 // 1. 预先确定品牌与菜单数据
const myBrand = { icon: 'shield-lock', label: '安全控制台' }; const myBrand = { icon: 'shield-lock', label: '安全控制台' };
const myNavList = [ const myNavList = [
{ type: 'label', label: '主导航', class: 'mt-1' },
{ type: 'button', name: 'dashboard', label: '仪表盘', icon: 'speedometer' }, { type: 'button', name: 'dashboard', label: '仪表盘', icon: 'speedometer' },
{ {
type: 'dropdown', type: 'dropdown',
@ -222,7 +223,8 @@ const { HTTP, UI, AutoForm, State } = window.ApigoBase
label: '系统设置', label: '系统设置',
icon: 'gear', icon: 'gear',
list: [ list: [
{ type: 'button', name: 'profile', label: '个人信息', icon: 'person' }, { type: 'label', label: '偏好设置' },
{ type: 'button', name: 'profile', label: '个人信息', icon: 'person', class: 'ps-2' },
{ type: 'switch', name: 'darkMode', label: '暗黑模式', icon: 'moon', bind: LocalStorage, name: 'darkMode' } { type: 'switch', name: 'darkMode', label: '暗黑模式', icon: 'moon', bind: LocalStorage, name: 'darkMode' }
] ]
}, },
@ -241,6 +243,9 @@ const { HTTP, UI, AutoForm, State } = window.ApigoBase
``` ```
* **AI 核心要点** * **AI 核心要点**
* 常规导航项被点击时(且 `noselect` 不为 true会自动更新全局 `Hash.nav = item.name` * 常规导航项被点击时(且 `noselect` 不为 true会自动更新全局 `Hash.nav = item.name`
* `label` 可在一级或二级菜单中作为分组标题使用,`class` 可附加 Bootstrap 工具类(如 `ps-2`)做缩进或样式微调。
* `dropdown` 子项默认只派发 `nav` 事件,不自动写入 `Hash.nav`,适合承载偏好设置、开关等功能菜单。
* 垂直模式下二级菜单以内联展开方式显示,外层布局需提供局部滚动边界。
--- ---

149
dist/base.js vendored
View File

@ -399,9 +399,31 @@
} }
globalThis.MouseMover = MouseMover; globalThis.MouseMover = MouseMover;
const APIComponent = globalThis.Component.register("API", (container) => { const APIComponent = globalThis.Component.register("API", (container) => {
container.request = globalThis.NewState({ url: "", method: "GET", headers: {}, data: null, timeout: 1e4, responseType: "" }); const presetRequest = container.request && typeof container.request === "object" ? container.request : {};
container.response = globalThis.NewState({ loading: false, ok: null, status: null, error: null, headers: {}, responseType: "", result: null }); const presetResponse = container.response && typeof container.response === "object" ? container.response : {};
container.result = globalThis.NewState(); const presetResult = container.result && typeof container.result === "object" ? container.result : {};
container.request = globalThis.NewState({
url: "",
method: "GET",
headers: {},
data: null,
timeout: 1e4,
responseType: "",
...presetRequest,
headers: { ...presetRequest.headers || {} }
});
container.response = globalThis.NewState({
loading: false,
ok: null,
status: null,
error: null,
headers: {},
responseType: "",
result: null,
...presetResponse,
headers: { ...presetResponse.headers || {} }
});
container.result = globalThis.NewState(presetResult);
container.do = (opt = {}) => { container.do = (opt = {}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = { ...container.request, ...opt }; const req = { ...container.request, ...opt };
@ -860,55 +882,104 @@
)); ));
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.click = (item, noselect) => { container.click = (item, noselect) => {
if (!item.noselect && !noselect) globalThis.Hash.nav = item.name; if (!item.noselect && !noselect) globalThis.Hash.nav = item.name;
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false })); container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
}; };
container.clickSubitem = (item) => {
container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
};
container.toggleGroup = (item) => {
container.state.openName = container.state.openName === item.name ? null : item.name;
container.dispatchEvent(new CustomEvent("nav", { detail: { item, open: container.state.openName === item.name }, bubbles: false }));
};
}, globalThis.Util.makeDom( }, globalThis.Util.makeDom(
/*html*/ /*html*/
` `
<div $class="\${this.vertical ? 'd-flex flex-column border-end h-100' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-3 \${this.vertical ? 'py-3' : 'pb-0'} align-items-center \${this.vertical ? 'align-items-start' : ''}"> <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="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-4' : 'me-2'}" style="height:30px;width:auto;max-width:300px"> <img $src="this.state.brand.image" $class="\${this.vertical ? 'mb-2' : 'me-2'}" style="height:30px;width:auto;max-width:300px">
</template> </template>
<template $if="this.state?.brand?.icon"> <template $if="this.state?.brand?.icon">
<i $class="bi bi-\${this.state.brand.icon} \${this.vertical ? 'mb-4' : 'me-2'}"></i> <i $class="bi bi-\${this.state.brand.icon} \${this.vertical ? 'mb-2' : 'me-2'}"></i>
</template> </template>
<template $if="this.state?.brand?.label"> <template $if="this.state?.brand?.label">
<span $class="\${this.vertical ? 'mb-4 fw-bold' : 'me-2'}" $text="this.state.brand.label"></span> <span $class="\${this.vertical ? 'mb-2 fw-bold' : 'me-2'}" style="transform: translateY(3px);" $text="this.state.brand.label"></span>
</template>
<div $class="\${this.vertical ? 'w-100' : 'ms-2'}"></div>
<template $each="this.state?.list || []">
<div $class="\${this.vertical ? 'nav nav-pills flex-column w-100' : 'navbar-nav'} text-truncate \${item.type==='fill'?'flex-fill':''}">
<template $if="item.type==='button'">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'text-start' : ''}" $onclick="this.click(item)">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? '' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
</template>
<template $if="item.type==='dropdown'">
<div class="dropdown">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'text-start' : ''}" data-bs-toggle="dropdown">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? '' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
<div $class="dropdown-menu \${this.vertical?'position-static':'dropdown-menu-end'} p-3 bg-body-secondary shadow" $style="width: \${item.width || 250}px;">
<template $each="item.list" as="subitem">
<template $if="subitem.type==='button'">
<button class="nav-link px-0 w-100 text-start" $onclick="this.click(subitem, true)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
</template>
<template $if="subitem.type==='switch'">
<div class="d-flex align-items-center">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch"><input class="form-check-input mx-0" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</template>
</div>
</div>
</template>
</div>
</template> </template>
</div>
<div $class="d-flex \${this.vertical ? 'w-100 flex-fill mt-2 flex-column' : 'ms-2 align-items-center flex-fill'}" $style="this.vertical ? 'min-height:0; overflow-y:auto; overflow-x:visible;' : ''">
<template $each="this.state?.list || []">
<div $class="\${this.vertical ? 'nav nav-pills flex-column w-100 position-relative' : 'navbar-nav flex-row align-items-center'} \${!this.vertical && item.type==='fill' ? 'flex-fill' : ''}" style="transform: translateY(3px);">
<template $if="item.type==='label'">
<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)">
<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>
<template $if="item.type==='dropdown'">
<template $if="this.vertical">
<div class="w-100">
<button type="button" $class="nav-link w-100 text-start py-1 px-2 d-flex align-items-center justify-content-between \${item.class || ''}" $onclick="this.toggleGroup(item)">
<span class="d-inline-flex align-items-center">
<i $class="bi bi-\${item.icon} me-2"></i><span class="text-truncate" $text="item.label"></span>
</span>
<i $class="bi \${this.state.openName===item.name?'bi-chevron-down':'bi-chevron-right'} small ms-2"></i>
</button>
<template $if="this.state.openName===item.name">
<div class="d-flex flex-column w-100 ps-2 mt-1">
<template $each="item.list" as="subitem">
<template $if="subitem.type==='label'">
<div $class="small text-uppercase text-body-secondary fw-semibold px-2 py-1 \${subitem.class || ''}" $text="subitem.label"></div>
</template>
<template $if="subitem.type==='button'">
<button $class="nav-link w-100 text-start py-1 px-2 \${subitem.class || ''}" $onclick="this.clickSubitem(subitem)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
</template>
<template $if="subitem.type==='switch'">
<div $class="d-flex align-items-center px-2 py-2 \${subitem.class || ''}">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch ms-2"><input class="form-check-input" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</template>
</div>
</template>
</div>
</template>
<template $if="!this.vertical">
<div class="dropdown">
<button type="button" $class="nav-link dropdown-toggle \${item.class || ''}" data-bs-toggle="dropdown">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="'d-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'" $text="item.label"></span>
</button>
<div class="dropdown-menu dropdown-menu-end p-2 bg-body-secondary shadow" $style="'width: ' + (item.width || 250) + 'px;'">
<template $each="item.list" as="subitem">
<template $if="subitem.type==='label'">
<div $class="dropdown-header \${subitem.class || ''}" $text="subitem.label"></div>
</template>
<template $if="subitem.type==='button'">
<button $class="dropdown-item \${subitem.class || ''}" $onclick="this.clickSubitem(subitem)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
</template>
<template $if="subitem.type==='switch'">
<div $class="d-flex align-items-center px-3 py-2 \${subitem.class || ''}">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch ms-2"><input class="form-check-input" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</template>
</div>
</div>
</template>
</template>
</div>
</template>
</div>
</div> </div>
` `
)); ));

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.20", "version": "1.0.21",
"type": "module", "type": "module",
"main": "dist/base.js", "main": "dist/base.js",
"files": [ "files": [

View File

@ -2,9 +2,32 @@
* API Component Module * API Component Module
*/ */
const APIComponent = globalThis.Component.register('API', container => { const APIComponent = globalThis.Component.register('API', container => {
container.request = globalThis.NewState({ url: '', method: 'GET', headers: {}, data: null, timeout: 10000, responseType: '' }) const presetRequest = container.request && typeof container.request === 'object' ? container.request : {}
container.response = globalThis.NewState({ loading: false, ok: null, status: null, error: null, headers: {}, responseType: '', result: null }) const presetResponse = container.response && typeof container.response === 'object' ? container.response : {}
container.result = globalThis.NewState() const presetResult = container.result && typeof container.result === 'object' ? container.result : {}
container.request = globalThis.NewState({
url: '',
method: 'GET',
headers: {},
data: null,
timeout: 10000,
responseType: '',
...presetRequest,
headers: { ...(presetRequest.headers || {}) }
})
container.response = globalThis.NewState({
loading: false,
ok: null,
status: null,
error: null,
headers: {},
responseType: '',
result: null,
...presetResponse,
headers: { ...(presetResponse.headers || {}) }
})
container.result = globalThis.NewState(presetResult)
container.do = (opt = {}) => { container.do = (opt = {}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = { ...container.request, ...opt } const req = { ...container.request, ...opt }

View File

@ -2,53 +2,102 @@
* Nav Component Module * Nav Component Module
*/ */
globalThis.Component.register('Nav', container => { globalThis.Component.register('Nav', container => {
container.vertical = container.hasAttribute('vertical') container.vertical = container.hasAttribute('vertical')
container.click = (item, noselect) => { container.state.openName = container.state.openName || null
if (!item.noselect && !noselect) globalThis.Hash.nav = item.name container.click = (item, noselect) => {
container.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false })) if (!item.noselect && !noselect) globalThis.Hash.nav = item.name
} container.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false }))
}
container.clickSubitem = item => {
container.dispatchEvent(new CustomEvent('nav', { detail: { item }, bubbles: false }))
}
container.toggleGroup = item => {
container.state.openName = container.state.openName === item.name ? null : item.name
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' : 'navbar navbar-expand border-bottom'} bg-body-secondary px-3 \${this.vertical ? 'py-3' : 'pb-0'} align-items-center \${this.vertical ? 'align-items-start' : ''}"> <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="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-4' : 'me-2'}" style="height:30px;width:auto;max-width:300px"> <img $src="this.state.brand.image" $class="\${this.vertical ? 'mb-2' : 'me-2'}" style="height:30px;width:auto;max-width:300px">
</template> </template>
<template $if="this.state?.brand?.icon"> <template $if="this.state?.brand?.icon">
<i $class="bi bi-\${this.state.brand.icon} \${this.vertical ? 'mb-4' : 'me-2'}"></i> <i $class="bi bi-\${this.state.brand.icon} \${this.vertical ? 'mb-2' : 'me-2'}"></i>
</template> </template>
<template $if="this.state?.brand?.label"> <template $if="this.state?.brand?.label">
<span $class="\${this.vertical ? 'mb-4 fw-bold' : 'me-2'}" $text="this.state.brand.label"></span> <span $class="\${this.vertical ? 'mb-2 fw-bold' : 'me-2'}" style="transform: translateY(3px);" $text="this.state.brand.label"></span>
</template>
<div $class="\${this.vertical ? 'w-100' : 'ms-2'}"></div>
<template $each="this.state?.list || []">
<div $class="\${this.vertical ? 'nav nav-pills flex-column w-100' : 'navbar-nav'} text-truncate \${item.type==='fill'?'flex-fill':''}">
<template $if="item.type==='button'">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'text-start' : ''}" $onclick="this.click(item)">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? '' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
</template>
<template $if="item.type==='dropdown'">
<div class="dropdown">
<button $class="nav-link \${Hash.nav===item.name?'active':''} \${this.vertical ? 'text-start' : ''}" data-bs-toggle="dropdown">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="\${this.vertical ? '' : 'd-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'}" $text="item.label"></span>
</button>
<div $class="dropdown-menu \${this.vertical?'position-static':'dropdown-menu-end'} p-3 bg-body-secondary shadow" $style="width: \${item.width || 250}px;">
<template $each="item.list" as="subitem">
<template $if="subitem.type==='button'">
<button class="nav-link px-0 w-100 text-start" $onclick="this.click(subitem, true)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
</template>
<template $if="subitem.type==='switch'">
<div class="d-flex align-items-center">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch"><input class="form-check-input mx-0" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</template>
</div>
</div>
</template>
</div>
</template> </template>
</div>
<div $class="d-flex \${this.vertical ? 'w-100 flex-fill mt-2 flex-column' : 'ms-2 align-items-center flex-fill'}" $style="this.vertical ? 'min-height:0; overflow-y:auto; overflow-x:visible;' : ''">
<template $each="this.state?.list || []">
<div $class="\${this.vertical ? 'nav nav-pills flex-column w-100 position-relative' : 'navbar-nav flex-row align-items-center'} \${!this.vertical && item.type==='fill' ? 'flex-fill' : ''}" style="transform: translateY(3px);">
<template $if="item.type==='label'">
<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)">
<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>
<template $if="item.type==='dropdown'">
<template $if="this.vertical">
<div class="w-100">
<button type="button" $class="nav-link w-100 text-start py-1 px-2 d-flex align-items-center justify-content-between \${item.class || ''}" $onclick="this.toggleGroup(item)">
<span class="d-inline-flex align-items-center">
<i $class="bi bi-\${item.icon} me-2"></i><span class="text-truncate" $text="item.label"></span>
</span>
<i $class="bi \${this.state.openName===item.name?'bi-chevron-down':'bi-chevron-right'} small ms-2"></i>
</button>
<template $if="this.state.openName===item.name">
<div class="d-flex flex-column w-100 ps-2 mt-1">
<template $each="item.list" as="subitem">
<template $if="subitem.type==='label'">
<div $class="small text-uppercase text-body-secondary fw-semibold px-2 py-1 \${subitem.class || ''}" $text="subitem.label"></div>
</template>
<template $if="subitem.type==='button'">
<button $class="nav-link w-100 text-start py-1 px-2 \${subitem.class || ''}" $onclick="this.clickSubitem(subitem)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
</template>
<template $if="subitem.type==='switch'">
<div $class="d-flex align-items-center px-2 py-2 \${subitem.class || ''}">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch ms-2"><input class="form-check-input" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</template>
</div>
</template>
</div>
</template>
<template $if="!this.vertical">
<div class="dropdown">
<button type="button" $class="nav-link dropdown-toggle \${item.class || ''}" data-bs-toggle="dropdown">
<i $class="bi bi-\${item.icon} me-2"></i><span $class="'d-none d-' + (this.state?.list?.length>5?'lg':'md') + '-inline'" $text="item.label"></span>
</button>
<div class="dropdown-menu dropdown-menu-end p-2 bg-body-secondary shadow" $style="'width: ' + (item.width || 250) + 'px;'">
<template $each="item.list" as="subitem">
<template $if="subitem.type==='label'">
<div $class="dropdown-header \${subitem.class || ''}" $text="subitem.label"></div>
</template>
<template $if="subitem.type==='button'">
<button $class="dropdown-item \${subitem.class || ''}" $onclick="this.clickSubitem(subitem)">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span>
</button>
</template>
<template $if="subitem.type==='switch'">
<div $class="d-flex align-items-center px-3 py-2 \${subitem.class || ''}">
<i $class="bi bi-\${subitem.icon} me-2 d-inline-block" style="width: 16px;"></i><span $text="subitem.label"></span><div class="flex-fill"></div>
<div class="form-switch ms-2"><input class="form-check-input" type="checkbox" $bind="subitem.bind[subitem.name]"></div>
</div>
</template>
</template>
</div>
</div>
</template>
</template>
</div>
</template>
</div>
</div> </div>
`)) `))

View File

@ -28,6 +28,35 @@ export async function runTests() {
if (!apiResp.ok) throw new Error('API component request failed'); if (!apiResp.ok) throw new Error('API component request failed');
api.remove(); api.remove();
console.log('Testing API declarative preset request fields...');
LocalStorage.secret = 'api-secret-token';
const presetApi = document.createElement('API');
presetApi.setAttribute('$.request.url', "'/echo'");
presetApi.setAttribute('$.request.method', "'POST'");
presetApi.setAttribute('$.request.data.secret', "LocalStorage.secret || ''");
document.body.appendChild(presetApi);
await new Promise(r => setTimeout(r, 50));
const originalFetch = globalThis.fetch;
let capturedBody = null;
globalThis.fetch = async (_url, options = {}) => {
capturedBody = options.body;
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
};
try {
const presetResp = await presetApi.do();
if (!presetResp.ok) throw new Error('Declarative API preset request failed');
} finally {
globalThis.fetch = originalFetch;
presetApi.remove();
}
if (capturedBody !== JSON.stringify({ secret: 'api-secret-token' })) {
throw new Error('API declarative preset request.data.secret was not preserved');
}
// 5. AutoForm & TagsInput Test // 5. AutoForm & TagsInput Test
console.log('Testing AutoForm...'); console.log('Testing AutoForm...');
const { NewState } = await import('@apigo.cc/state'); const { NewState } = await import('@apigo.cc/state');