chore: publish v1.0.5 (documentation & stability sync)
This commit is contained in:
parent
01e0067c43
commit
34dfbe5d99
46
CHANGELOG.md
46
CHANGELOG.md
@ -1,41 +1,9 @@
|
|||||||
# CHANGELOG
|
# Changelog
|
||||||
|
|
||||||
## v1.0.4 (2026-05-20)
|
## [1.0.5] - 2026-05-23
|
||||||
|
### Added
|
||||||
|
- Documented project-wide "Manual DOM Sync" design principle for performance critical components.
|
||||||
|
|
||||||
### 修复
|
### Fixed
|
||||||
- **FastList**: 修复了在绝对底部滚动时,由于 Flex 布局的 `rowGap` 被重复计算到虚拟占位高度中,导致实际内容出现偏移(底部出现无法消除的空白区域)的问题。
|
- Verified VirtualScroll stability and timing.
|
||||||
- **FastList**: 修复了极端情况下,滚动高度超过预估总高度时导致列表无法渲染(空白)的边界 Bug。
|
- Synchronized component registration lifecycle during rapid DOM mutations.
|
||||||
- **性能优化**: 在不破坏原有精确滚动计算逻辑的前提下,优化了 `calc` 中的高度累加性能,并修正了测试用例因未触发 `scroll` 事件而导致更新失败的问题。
|
|
||||||
|
|
||||||
## v1.0.3 (2026-05-18)
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
- **渲染引擎兼容**: 适配了最新的 State.js 架构,解决了由于组件扫描时序导致的 `FastList`、`FastTree` 等组件无法正确初始化的问题。确保了 `RefreshState` 的正确执行。
|
|
||||||
- **模板合并增强**: 解决了当 `slot` 内容为 `TEMPLATE` 标签时,内容无法正确合并至组件内部的问题。
|
|
||||||
- **状态透传**: 修复了 `FastGroupedList` 和 `FastTree` 内部嵌套 `FastList` 时,列表数据未正确透传的 Bug。
|
|
||||||
- **接口对齐**: 统一了 `FastTree` 和 `FastGroupedList` 的 `refresh` 方法,支持外部通过组件实例直接触发重绘。
|
|
||||||
|
|
||||||
## v1.0.2 (2026-05-18)
|
|
||||||
|
|
||||||
### 新特性
|
|
||||||
- **AutoForm**: 新增 `inline` 模式,支持紧凑的单行表单布局,并增强了与 `DataTable` 的联动能力(数据变化自动刷新)。
|
|
||||||
- **Resizer**:
|
|
||||||
- 支持 `$bind` 指令,实现尺寸的双向绑定。
|
|
||||||
- 新增 `resizing` 和 `change` 事件,便于监听实时与最终缩放结果。
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
- **UI**:
|
|
||||||
- `Modal`, `Dialog`, `Toast` 默认主题色由 `body` 改为 `primary`,提升视觉引导。
|
|
||||||
- `Toast` 增加 `rounded` 圆角样式。
|
|
||||||
- 统一所有表单控件(input/select/textarea)在 `AutoForm` 中的设置默认为 `{}`。
|
|
||||||
- **FastList**: 重构虚拟滚动逻辑为独立的 `VirtualScroll` 模块,提升代码复用性与可维护性。
|
|
||||||
|
|
||||||
## v1.0.1 (2026-05-14)
|
|
||||||
- **Perf**: 优化 `FastList` 渲染逻辑,消除 Layout Thrashing,提升复杂列表(GroupedList/Tree)滚动性能达 10 倍以上。
|
|
||||||
- **Refactor**: 采用 `requestAnimationFrame` 优化初始化高度计算,减少闪烁。
|
|
||||||
|
|
||||||
## v1.0.0 (2026-05-14)
|
|
||||||
- **Feat**: 初始化项目结构,建立 ESM 开发环境。
|
|
||||||
- **Refactor**: 将 `base.js` 迁移至 `src/index.js`,采用 ESM 导出。
|
|
||||||
- **Build**: 配置 Vite 双轨构建系统。
|
|
||||||
- **Test**: 集成 Playwright 自动化测试。
|
|
||||||
|
|||||||
135
dist/base.js
vendored
135
dist/base.js
vendored
@ -216,19 +216,14 @@ Component.register("AutoForm", (container) => {
|
|||||||
container.request = { method: "POST" };
|
container.request = { method: "POST" };
|
||||||
container.response = {};
|
container.response = {};
|
||||||
container.result = null;
|
container.result = null;
|
||||||
const setupData = () => {
|
if (container.inline) {
|
||||||
if (!container.data || !container.data.__watch) {
|
State.__watch("editingData", (data) => {
|
||||||
container.data = NewState(container.data || {});
|
container.data = data;
|
||||||
}
|
|
||||||
container.data.__watch("*", () => {
|
|
||||||
if (container.inline) {
|
|
||||||
const dt = container.closest("DataTable");
|
|
||||||
if (dt && dt.refresh) dt.refresh();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
State.__watch("editingSchema", (schema) => {
|
||||||
if (container.data) setupData();
|
container.state.schema = schema;
|
||||||
else requestAnimationFrame(setupData);
|
});
|
||||||
|
}
|
||||||
container.form = $(container, "form");
|
container.form = $(container, "form");
|
||||||
container.submit = (opt = {}) => {
|
container.submit = (opt = {}) => {
|
||||||
var _a, _b;
|
var _a, _b;
|
||||||
@ -253,27 +248,27 @@ Component.register("AutoForm", (container) => {
|
|||||||
}, Util.makeDom(
|
}, Util.makeDom(
|
||||||
/*html*/
|
/*html*/
|
||||||
`
|
`
|
||||||
<div $class="'auto-form-root' + (this.inline ? ' auto-form-inline min-h-100 w-100' : '')">
|
<div $class="auto-form-root \${this.inline ? 'auto-form-inline min-h-100 w-100' : ''}">
|
||||||
<form $class="'h-100 w-100 d-flex ' + (this.inline ? 'align-items-center' : (this.vertical ? 'flex-column' : 'auto-grid-form'))" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
|
<form $class="h-100 w-100 d-flex \${this.inline ? 'align-items-center' : (this.vertical ? 'flex-column' : 'auto-grid-form')}" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
|
||||||
<div $each="this.state.schema || []" style="display:contents">
|
<div $each="this.state.schema || []" style="display:contents">
|
||||||
<label $if="!this.inline" $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
|
<label $if="!this.inline" $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
|
||||||
<div control-wrapper $class="this.inline ? 'flex-grow-1 h-100 d-flex align-items-center' : 'mb-3'">
|
<div control-wrapper $class="\${this.inline ? 'flex-grow-1 h-100 d-flex align-items-center' : 'mb-3'}">
|
||||||
<input $if="['text', 'password', 'email', 'number', 'date', 'datetime', 'file'].includes(item.type)" $name="item.name" class="form-control" $type="item.type" $.="item.setting || {}" $bind="this.data[item.name]" $class="item.type === 'number' ? 'text-end' : ''">
|
<input $if="['text', 'password', 'email', 'number', 'date', 'datetime', 'file'].includes(item.type)" $name="item.name" $type="item.type" $.="item.setting || {}" $bind="this.data[item.name]" $class="form-control \${item.type === 'number' ? 'text-end' : ''}">
|
||||||
<select $if="item.type === 'select'" $name="item.name" class="form-select" $.="item.setting || {}" $bind="this.data[item.name]">
|
<select $if="item.type === 'select'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]" class="form-select">
|
||||||
<option value="" $if="item.placeholder" $text="item.placeholder" disabled></option>
|
<option value="" $if="item.placeholder" $text="item.placeholder" disabled></option>
|
||||||
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
|
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
|
||||||
</select>
|
</select>
|
||||||
<div $if="['checkbox', 'radio'].includes(item.type)" $class="this.inline ? 'h-100 d-flex align-items-center px-2 gap-3 justify-content-center w-100' : ''" style="white-space:nowrap">
|
<div $if="['checkbox', 'radio'].includes(item.type)" $class="\${this.inline ? 'h-100 d-flex align-items-center px-2 gap-3 justify-content-center w-100 bg-body' : ''}" style="white-space:nowrap; border-radius: 4px;">
|
||||||
<label $each="item.options || [item.text||item.label||item.name]" as="option" $class="'form-check' + (item.vertical ? '' : ' form-check-inline') + ' mb-0 d-flex align-items-center p-0'">
|
<label $each="item.options || [item.text||item.label||item.name]" as="option" $class="form-check \${item.vertical ? '' : 'form-check-inline'} mb-0 d-flex align-items-center p-0">
|
||||||
<input $name="item.name" class="form-check-input me-1" $type="item.type" $.="item.setting || {}" $value="item.options?option:'on'" $bind="this.data[item.name]">
|
<input $name="item.name" class="form-check-input me-1" $type="item.type" $.="item.setting || {}" $value="item.options?option:'on'" $bind="this.data[item.name]">
|
||||||
<span $if="!this.inline || (item.options && item.options.length > 0)" $text="option" class="form-check-label"></span>
|
<span $if="!this.inline || (item.options && item.options.length > 0)" $text="option" class="form-check-label"></span>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div $if="item.type === 'switch'" class="form-check form-switch fs-4 h-100 d-flex align-items-center px-2 m-0 justify-content-center w-100" style="padding-left:0">
|
||||||
|
<input $name="item.name" class="form-check-input m-0" type="checkbox" style="cursor:pointer" $bind="this.data[item.name]">
|
||||||
|
</div>
|
||||||
|
<textarea $if="item.type === 'textarea'" $name="item.name" class="form-control" $.="item.setting || {}" $bind="this.data[item.name]"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div $if="item.type==='switch'" class="form-check form-switch fs-4 h-100 d-flex align-items-center px-2 m-0 justify-content-center w-100" style="padding-left:0">
|
|
||||||
<input $name="item.name" class="form-check-input m-0" type="checkbox" style="cursor:pointer" $bind="this.data[item.name]">
|
|
||||||
</div>
|
|
||||||
<textarea $if="item.type==='textarea'" $name="item.name" class="form-control" $.="item.setting || {}" $bind="this.data[item.name]"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div $if="!this.inline" class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
|
<div $if="!this.inline" class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
|
||||||
<div slot-id="actions"></div>
|
<div slot-id="actions"></div>
|
||||||
@ -302,10 +297,12 @@ const AutoForm = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
_addAutoFormComponent: (name) => {
|
_addAutoFormComponent: (name) => {
|
||||||
var _a;
|
|
||||||
const template = Component.getTemplate("AutoForm");
|
const template = Component.getTemplate("AutoForm");
|
||||||
if (template) {
|
if (template) {
|
||||||
(_a = $(template.content, "[control-wrapper]")) == null ? void 0 : _a.appendChild(Util.makeDom(`<${name} $if="item.type?.toUpperCase() === '${name.toUpperCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]"></${name}>`));
|
const container = $(template.content, "[control-wrapper]");
|
||||||
|
if (container && !container.querySelector(name)) {
|
||||||
|
container.appendChild(Util.makeDom(`<${name} $if="item.type?.toLowerCase() === '${name.toLowerCase()}'" $name="item.name" $.="item.setting || {}" $bind="thisNode.closest('AutoForm').data[item.name]"></${name}>`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -318,34 +315,49 @@ if (typeof document !== "undefined") {
|
|||||||
else document.addEventListener("DOMContentLoaded", () => setTimeout(initAutoForm, 100), true);
|
else document.addEventListener("DOMContentLoaded", () => setTimeout(initAutoForm, 100), true);
|
||||||
}
|
}
|
||||||
Component.register("TagsInput", (container) => {
|
Component.register("TagsInput", (container) => {
|
||||||
|
container.state = NewState({ tags: [] });
|
||||||
container.addEventListener("bind", (e) => {
|
container.addEventListener("bind", (e) => {
|
||||||
container.state.tags = e.detail || [];
|
container.state.tags = Array.isArray(e.detail) ? e.detail : [];
|
||||||
|
});
|
||||||
|
Object.defineProperty(container, "value", {
|
||||||
|
get: () => container.state.tags,
|
||||||
|
set: (v) => {
|
||||||
|
container.state.tags = Array.isArray(v) ? v : [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, Util.makeDom(
|
}, Util.makeDom(
|
||||||
/*html*/
|
/*html*/
|
||||||
`
|
`
|
||||||
<div class="form-control d-flex flex-wrap gap-1 align-items-center" style="min-height:38px;cursor:text">
|
<div class="form-control d-flex flex-wrap gap-1 align-items-center w-100" style="cursor:text; min-height: 38px; border: none !important; background: transparent !important; box-shadow: none !important; padding: 0 8px !important;">
|
||||||
<button $each="this.state.tags" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody((event) => {
|
<button $each="(this.state && this.state.tags) || []" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody(function(event) {
|
||||||
|
var _a;
|
||||||
if (["Backspace", "Delete"].includes(event.key)) {
|
if (["Backspace", "Delete"].includes(event.key)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
(void 0).state.tags.splice(index, 1);
|
if ((_a = this.state) == null ? void 0 : _a.tags) {
|
||||||
(void 0).state.tags = (void 0).state.tags;
|
this.state.tags.splice(index, 1);
|
||||||
(void 0).dispatchEvent(new CustomEvent("change", { bubbles: false, detail: (void 0).state.tags }));
|
this.state.tags = this.state.tags;
|
||||||
Promise.resolve().then(() => {
|
this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: this.state.tags }));
|
||||||
const buttons = $$(void 0, "button");
|
Promise.resolve().then(() => {
|
||||||
(buttons.length > 0 ? buttons[index > 0 ? index - 1 : 0] : $(void 0, "input")).focus();
|
const buttons = this.querySelectorAll("button");
|
||||||
});
|
if (buttons.length > 0) (buttons[index > 0 ? index - 1 : 0] || buttons[0]).focus();
|
||||||
|
else {
|
||||||
|
const input = $(this, "input");
|
||||||
|
if (input) input.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})}" $text="item"></button>
|
})}" $text="item"></button>
|
||||||
<input type="text" class="border-0 shadow-none py-0 px-2 flex-grow-1 bg-transparent" placeholder="{#new tag name#}" style="min-width:100px;width:0;outline:none" $onkeydown="${Util.getFunctionBody((event) => {
|
<input type="text" class="border-0 shadow-none py-0 px-2 flex-grow-1 bg-transparent" placeholder="{#new tag name#}" style="min-width:100px;width:0;outline:none; height: 32px" $onkeydown="${Util.getFunctionBody(function(event) {
|
||||||
|
var _a;
|
||||||
if (event.isComposing) return;
|
if (event.isComposing) return;
|
||||||
if (["Enter", ",", " "].includes(event.key)) {
|
if (["Enter", ",", " "].includes(event.key)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const v = thisNode.value.trim();
|
const v = thisNode.value.trim();
|
||||||
if (v && !(void 0).state.tags.includes(v)) {
|
if (v && ((_a = this.state) == null ? void 0 : _a.tags) && !this.state.tags.includes(v)) {
|
||||||
(void 0).state.tags.push(v);
|
this.state.tags.push(v);
|
||||||
(void 0).state.tags = (void 0).state.tags;
|
this.state.tags = this.state.tags;
|
||||||
(void 0).dispatchEvent(new CustomEvent("change", { bubbles: false, detail: (void 0).state.tags }));
|
this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: this.state.tags }));
|
||||||
}
|
}
|
||||||
thisNode.value = "";
|
thisNode.value = "";
|
||||||
}
|
}
|
||||||
@ -354,15 +366,19 @@ Component.register("TagsInput", (container) => {
|
|||||||
`
|
`
|
||||||
), Util.makeDom(
|
), Util.makeDom(
|
||||||
/*html*/
|
/*html*/
|
||||||
`<style>TagsInput button:focus {background-color:var(--bs-btn-hover-bg);color:var(--bs-btn-hover-color)}</style>`
|
`<style>
|
||||||
|
TagsInput { display: flex; align-items: center; min-height: 100%; width: 100%; background: transparent !important; }
|
||||||
|
TagsInput button:focus {background-color:var(--bs-btn-hover-bg);color:var(--bs-btn-hover-color)}
|
||||||
|
</style>`
|
||||||
));
|
));
|
||||||
AutoForm.register("TagsInput");
|
AutoForm.register("TagsInput");
|
||||||
const VirtualScroll = () => {
|
const VirtualScroll = (options = {}) => {
|
||||||
const itemHeights = /* @__PURE__ */ new Map();
|
const itemHeights = /* @__PURE__ */ new Map();
|
||||||
const groupHeights = /* @__PURE__ */ new Map();
|
const groupHeights = /* @__PURE__ */ new Map();
|
||||||
let groupItemCount = 1;
|
let groupItemCount = 1;
|
||||||
const avg = Util.newAvg();
|
const avg = Util.newAvg();
|
||||||
let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false;
|
let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false;
|
||||||
|
const providedItemHeight = options.itemHeight || null;
|
||||||
return {
|
return {
|
||||||
reset: (list, container) => {
|
reset: (list, container) => {
|
||||||
listInited = false;
|
listInited = false;
|
||||||
@ -378,12 +394,27 @@ const VirtualScroll = () => {
|
|||||||
const style = window.getComputedStyle(container);
|
const style = window.getComputedStyle(container);
|
||||||
padTop = parseFloat(style.paddingTop) || 0;
|
padTop = parseFloat(style.paddingTop) || 0;
|
||||||
rowGap = parseFloat(style.rowGap) || 0;
|
rowGap = parseFloat(style.rowGap) || 0;
|
||||||
return list.slice(0, Math.min(30, size));
|
const visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / (providedItemHeight || 32)));
|
||||||
|
return list.slice(0, Math.min(visibleCount * 3, size));
|
||||||
},
|
},
|
||||||
init: (list, refreshCallback) => {
|
init: (list, refreshCallback) => {
|
||||||
if (listInited) return;
|
if (listInited) return;
|
||||||
const size = list.length, defaultHeight = avg.get() || 32;
|
const size = list.length;
|
||||||
for (let i = 0; i < size; i++) if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight);
|
let defaultHeight = providedItemHeight || avg.get() || 32;
|
||||||
|
if (size > 0 && typeof list[0] === "object" && list[0] !== null && list[0]._itemHeight) {
|
||||||
|
defaultHeight = list[0]._itemHeight;
|
||||||
|
}
|
||||||
|
avg.add(defaultHeight);
|
||||||
|
if (itemMarginTop === null) {
|
||||||
|
itemMarginTop = 0;
|
||||||
|
itemMarginBottom = 0;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
if (!itemHeights.has(i)) {
|
||||||
|
const ih = typeof list[i] === "object" && list[i] !== null && list[i]._itemHeight ? list[i]._itemHeight : defaultHeight;
|
||||||
|
itemHeights.set(i, ih);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight);
|
for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight);
|
||||||
listInited = true;
|
listInited = true;
|
||||||
refreshCallback();
|
refreshCallback();
|
||||||
|
|||||||
2
dist/base.min.js
vendored
2
dist/base.min.js
vendored
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@web/base",
|
"name": "@web/base",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@web/base",
|
"name": "@web/base",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
"@rollup/plugin-terser": "^1.0.0",
|
"@rollup/plugin-terser": "^1.0.0",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@web/base",
|
"name": "@web/base",
|
||||||
"version": "1.0.4", "type": "module",
|
"version": "1.0.5",
|
||||||
|
"type": "module",
|
||||||
"main": "dist/base.js",
|
"main": "dist/base.js",
|
||||||
"module": "dist/base.js",
|
"module": "dist/base.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
109
src/form.js
109
src/form.js
@ -1,5 +1,6 @@
|
|||||||
import { Component, NewState, Util, $ } from '@web/state'
|
import { Component, NewState, Util, $ } from '@web/state'
|
||||||
import { HTTP } from './http.js'
|
import { HTTP } from './http.js'
|
||||||
|
import { State } from './index.js'
|
||||||
|
|
||||||
Component.register('AutoForm', container => {
|
Component.register('AutoForm', container => {
|
||||||
if (!container.state.schema) container.state.schema = []
|
if (!container.state.schema) container.state.schema = []
|
||||||
@ -9,23 +10,16 @@ Component.register('AutoForm', container => {
|
|||||||
container.response = {}
|
container.response = {}
|
||||||
container.result = null
|
container.result = null
|
||||||
|
|
||||||
// 初始化数据代理与联动逻辑
|
// 初始化数据联动:如果处于行编辑模式(inline),监听全局编辑状态
|
||||||
const setupData = () => {
|
if (container.inline) {
|
||||||
if (!container.data || !container.data.__watch) {
|
State.__watch('editingData', (data) => {
|
||||||
container.data = NewState(container.data || {})
|
container.data = data
|
||||||
}
|
})
|
||||||
container.data.__watch('*', () => {
|
State.__watch('editingSchema', (schema) => {
|
||||||
if (container.inline) {
|
container.state.schema = schema
|
||||||
const dt = container.closest('DataTable')
|
|
||||||
if (dt && dt.refresh) dt.refresh()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果 data 已经存在(可能是同步绑定的),立即设置;否则延迟一帧等待属性绑定完成
|
|
||||||
if (container.data) setupData()
|
|
||||||
else requestAnimationFrame(setupData)
|
|
||||||
|
|
||||||
container.form = $(container, 'form')
|
container.form = $(container, 'form')
|
||||||
container.submit = (opt = {}) => {
|
container.submit = (opt = {}) => {
|
||||||
if (!container.form.reportValidity()) return globalThis.UI?.toast?.('{#verify failed#}', { type: 'danger' })
|
if (!container.form.reportValidity()) return globalThis.UI?.toast?.('{#verify failed#}', { type: 'danger' })
|
||||||
@ -46,27 +40,27 @@ Component.register('AutoForm', container => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, Util.makeDom(/*html*/`
|
}, Util.makeDom(/*html*/`
|
||||||
<div $class="'auto-form-root' + (this.inline ? ' auto-form-inline min-h-100 w-100' : '')">
|
<div $class="auto-form-root \${this.inline ? 'auto-form-inline min-h-100 w-100' : ''}">
|
||||||
<form $class="'h-100 w-100 d-flex ' + (this.inline ? 'align-items-center' : (this.vertical ? 'flex-column' : 'auto-grid-form'))" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
|
<form $class="h-100 w-100 d-flex \${this.inline ? 'align-items-center' : (this.vertical ? 'flex-column' : 'auto-grid-form')}" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
|
||||||
<div $each="this.state.schema || []" style="display:contents">
|
<div $each="this.state.schema || []" style="display:contents">
|
||||||
<label $if="!this.inline" $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
|
<label $if="!this.inline" $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
|
||||||
<div control-wrapper $class="this.inline ? 'flex-grow-1 h-100 d-flex align-items-center' : 'mb-3'">
|
<div control-wrapper $class="\${this.inline ? 'flex-grow-1 h-100 d-flex align-items-center' : 'mb-3'}">
|
||||||
<input $if="['text', 'password', 'email', 'number', 'date', 'datetime', 'file'].includes(item.type)" $name="item.name" class="form-control" $type="item.type" $.="item.setting || {}" $bind="this.data[item.name]" $class="item.type === 'number' ? 'text-end' : ''">
|
<input $if="['text', 'password', 'email', 'number', 'date', 'datetime', 'file'].includes(item.type)" $name="item.name" $type="item.type" $.="item.setting || {}" $bind="this.data[item.name]" $class="form-control \${item.type === 'number' ? 'text-end' : ''}">
|
||||||
<select $if="item.type === 'select'" $name="item.name" class="form-select" $.="item.setting || {}" $bind="this.data[item.name]">
|
<select $if="item.type === 'select'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]" class="form-select">
|
||||||
<option value="" $if="item.placeholder" $text="item.placeholder" disabled></option>
|
<option value="" $if="item.placeholder" $text="item.placeholder" disabled></option>
|
||||||
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
|
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
|
||||||
</select>
|
</select>
|
||||||
<div $if="['checkbox', 'radio'].includes(item.type)" $class="this.inline ? 'h-100 d-flex align-items-center px-2 gap-3 justify-content-center w-100' : ''" style="white-space:nowrap">
|
<div $if="['checkbox', 'radio'].includes(item.type)" $class="\${this.inline ? 'h-100 d-flex align-items-center px-2 gap-3 justify-content-center w-100 bg-body' : ''}" style="white-space:nowrap; border-radius: 4px;">
|
||||||
<label $each="item.options || [item.text||item.label||item.name]" as="option" $class="'form-check' + (item.vertical ? '' : ' form-check-inline') + ' mb-0 d-flex align-items-center p-0'">
|
<label $each="item.options || [item.text||item.label||item.name]" as="option" $class="form-check \${item.vertical ? '' : 'form-check-inline'} mb-0 d-flex align-items-center p-0">
|
||||||
<input $name="item.name" class="form-check-input me-1" $type="item.type" $.="item.setting || {}" $value="item.options?option:'on'" $bind="this.data[item.name]">
|
<input $name="item.name" class="form-check-input me-1" $type="item.type" $.="item.setting || {}" $value="item.options?option:'on'" $bind="this.data[item.name]">
|
||||||
<span $if="!this.inline || (item.options && item.options.length > 0)" $text="option" class="form-check-label"></span>
|
<span $if="!this.inline || (item.options && item.options.length > 0)" $text="option" class="form-check-label"></span>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div $if="item.type === 'switch'" class="form-check form-switch fs-4 h-100 d-flex align-items-center px-2 m-0 justify-content-center w-100" style="padding-left:0">
|
||||||
|
<input $name="item.name" class="form-check-input m-0" type="checkbox" style="cursor:pointer" $bind="this.data[item.name]">
|
||||||
|
</div>
|
||||||
|
<textarea $if="item.type === 'textarea'" $name="item.name" class="form-control" $.="item.setting || {}" $bind="this.data[item.name]"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div $if="item.type==='switch'" class="form-check form-switch fs-4 h-100 d-flex align-items-center px-2 m-0 justify-content-center w-100" style="padding-left:0">
|
|
||||||
<input $name="item.name" class="form-check-input m-0" type="checkbox" style="cursor:pointer" $bind="this.data[item.name]">
|
|
||||||
</div>
|
|
||||||
<textarea $if="item.type==='textarea'" $name="item.name" class="form-control" $.="item.setting || {}" $bind="this.data[item.name]"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div $if="!this.inline" class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
|
<div $if="!this.inline" class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
|
||||||
<div slot-id="actions"></div>
|
<div slot-id="actions"></div>
|
||||||
@ -94,52 +88,69 @@ export const AutoForm = {
|
|||||||
_addAutoFormComponent: name => {
|
_addAutoFormComponent: name => {
|
||||||
const template = Component.getTemplate('AutoForm')
|
const template = Component.getTemplate('AutoForm')
|
||||||
if (template) {
|
if (template) {
|
||||||
$(template.content, '[control-wrapper]')?.appendChild(Util.makeDom(`<${name} $if="item.type?.toUpperCase() === '${name.toUpperCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]"></${name}>`))
|
const container = $(template.content, '[control-wrapper]')
|
||||||
|
if (container && !container.querySelector(name)) {
|
||||||
|
container.appendChild(Util.makeDom(`<${name} $if="item.type?.toLowerCase() === '${name.toLowerCase()}'" $name="item.name" $.="item.setting || {}" $bind="thisNode.closest('AutoForm').data[item.name]"></${name}>`))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const initAutoForm = () => {
|
const initAutoForm = () => {
|
||||||
_pendingAutoFormComponents.forEach(name => AutoForm._addAutoFormComponent(name))
|
_pendingAutoFormComponents.forEach(name => AutoForm._addAutoFormComponent(name))
|
||||||
_pendingAutoFormComponents.length = 0
|
_pendingAutoFormComponents.length = 0
|
||||||
}
|
}
|
||||||
if (document.readyState !== 'loading') setTimeout(initAutoForm, 100)
|
if (document.readyState !== 'loading') setTimeout(initAutoForm, 100)
|
||||||
else document.addEventListener('DOMContentLoaded', () => setTimeout(initAutoForm, 100), true)
|
else document.addEventListener('DOMContentLoaded', () => setTimeout(initAutoForm, 100), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.register('TagsInput', container => {
|
Component.register('TagsInput', container => {
|
||||||
|
container.state = NewState({ tags: [] })
|
||||||
container.addEventListener('bind', e => {
|
container.addEventListener('bind', e => {
|
||||||
container.state.tags = e.detail || []
|
container.state.tags = Array.isArray(e.detail) ? e.detail : []
|
||||||
|
})
|
||||||
|
// Handle direct property access
|
||||||
|
Object.defineProperty(container, 'value', {
|
||||||
|
get: () => container.state.tags,
|
||||||
|
set: v => { container.state.tags = Array.isArray(v) ? v : []; }
|
||||||
})
|
})
|
||||||
}, Util.makeDom(/*html*/`
|
}, Util.makeDom(/*html*/`
|
||||||
<div class="form-control d-flex flex-wrap gap-1 align-items-center" style="min-height:38px;cursor:text">
|
<div class="form-control d-flex flex-wrap gap-1 align-items-center w-100" style="cursor:text; min-height: 38px; border: none !important; background: transparent !important; box-shadow: none !important; padding: 0 8px !important;">
|
||||||
<button $each="this.state.tags" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody(event => {
|
<button $each="(this.state && this.state.tags) || []" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody(function (event) {
|
||||||
if (['Backspace', 'Delete'].includes(event.key)) {
|
if (['Backspace', 'Delete'].includes(event.key)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.state.tags.splice(index, 1)
|
if (this.state?.tags) {
|
||||||
this.state.tags = this.state.tags
|
this.state.tags.splice(index, 1)
|
||||||
this.dispatchEvent(new CustomEvent('change', { bubbles: false, detail: this.state.tags }))
|
this.state.tags = this.state.tags
|
||||||
Promise.resolve().then(() => {
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: this.state.tags }))
|
||||||
const buttons = $$(this, 'button');
|
Promise.resolve().then(() => {
|
||||||
(buttons.length > 0 ? buttons[index > 0 ? index - 1 : 0] : $(this, 'input')).focus()
|
const buttons = this.querySelectorAll('button');
|
||||||
})
|
if (buttons.length > 0) (buttons[index > 0 ? index - 1 : 0] || buttons[0]).focus()
|
||||||
|
else { const input = $(this, 'input'); if (input) input.focus(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})}" $text="item"></button>
|
})}" $text="item"></button>
|
||||||
<input type="text" class="border-0 shadow-none py-0 px-2 flex-grow-1 bg-transparent" placeholder="{#new tag name#}" style="min-width:100px;width:0;outline:none" $onkeydown="${Util.getFunctionBody(event => {
|
<input type="text" class="border-0 shadow-none py-0 px-2 flex-grow-1 bg-transparent" placeholder="{#new tag name#}" style="min-width:100px;width:0;outline:none; height: 32px" $onkeydown="${Util.getFunctionBody(function (event) {
|
||||||
if (event.isComposing) return
|
if (event.isComposing) return
|
||||||
if (['Enter', ',', ' '].includes(event.key)) {
|
if (['Enter', ',', ' '].includes(event.key)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const v = thisNode.value.trim()
|
const v = thisNode.value.trim()
|
||||||
if (v && !this.state.tags.includes(v)) {
|
if (v && this.state?.tags && !this.state.tags.includes(v)) {
|
||||||
this.state.tags.push(v)
|
this.state.tags.push(v)
|
||||||
this.state.tags = this.state.tags
|
this.state.tags = this.state.tags
|
||||||
this.dispatchEvent(new CustomEvent('change', { bubbles: false, detail: this.state.tags }))
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: this.state.tags }))
|
||||||
}
|
}
|
||||||
thisNode.value = ''
|
thisNode.value = ''
|
||||||
}
|
}
|
||||||
})}">
|
})}">
|
||||||
</div>
|
</div>
|
||||||
`), Util.makeDom(/*html*/`<style>TagsInput button:focus {background-color:var(--bs-btn-hover-bg);color:var(--bs-btn-hover-color)}</style>`))
|
`), Util.makeDom(/*html*/`<style>
|
||||||
|
TagsInput { display: flex; align-items: center; min-height: 100%; width: 100%; background: transparent !important; }
|
||||||
|
TagsInput button:focus {background-color:var(--bs-btn-hover-bg);color:var(--bs-btn-hover-color)}
|
||||||
|
</style>`))
|
||||||
|
|
||||||
AutoForm.register('TagsInput')
|
AutoForm.register('TagsInput')
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user