2026-05-14 20:04:31 +08:00
|
|
|
|
import { Component, NewState, Util, $ } from '@web/state'
|
|
|
|
|
|
import { HTTP } from './http.js'
|
2026-05-23 17:37:24 +08:00
|
|
|
|
import { State } from './index.js'
|
2026-05-14 20:04:31 +08:00
|
|
|
|
|
|
|
|
|
|
Component.register('AutoForm', container => {
|
|
|
|
|
|
if (!container.state.schema) container.state.schema = []
|
|
|
|
|
|
container.vertical = container.hasAttribute('vertical')
|
2026-05-17 16:59:44 +08:00
|
|
|
|
container.inline = container.hasAttribute('inline')
|
2026-05-14 20:04:31 +08:00
|
|
|
|
container.request = { method: 'POST' }
|
|
|
|
|
|
container.response = {}
|
|
|
|
|
|
container.result = null
|
2026-05-17 17:03:18 +08:00
|
|
|
|
|
2026-05-23 17:37:24 +08:00
|
|
|
|
// 初始化数据联动:如果处于行编辑模式(inline),监听全局编辑状态
|
|
|
|
|
|
if (container.inline) {
|
|
|
|
|
|
State.__watch('editingData', (data) => {
|
|
|
|
|
|
container.data = data
|
|
|
|
|
|
})
|
|
|
|
|
|
State.__watch('editingSchema', (schema) => {
|
|
|
|
|
|
container.state.schema = schema
|
2026-05-17 17:03:18 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 20:04:31 +08:00
|
|
|
|
container.form = $(container, 'form')
|
|
|
|
|
|
container.submit = (opt = {}) => {
|
|
|
|
|
|
if (!container.form.reportValidity()) return globalThis.UI?.toast?.('{#verify failed#}', { type: 'danger' })
|
|
|
|
|
|
if (!container.dispatchEvent(new CustomEvent('submit', { detail: container.data, cancelable: true, bubbles: false }))) return
|
|
|
|
|
|
const req = { ...container.request, data: container.data, noui: true, ...opt }
|
|
|
|
|
|
let task = null
|
|
|
|
|
|
if (container.api) task = container.api.do(req)
|
|
|
|
|
|
else if (container.request.url) task = HTTP.request(req)
|
|
|
|
|
|
else return console.warn('{#please config .api or .request.url to auto submit#}')
|
|
|
|
|
|
task.then(resp => {
|
|
|
|
|
|
container.response = resp
|
|
|
|
|
|
container.result = resp.result
|
|
|
|
|
|
if (typeof resp.result === 'object' && resp.result.error) throw new Error(resp.result.error)
|
|
|
|
|
|
container.dispatchEvent(new CustomEvent('response', { detail: resp, bubbles: false }))
|
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
if (globalThis.UI?.toast) UI.toast(err.message, { type: 'danger' })
|
|
|
|
|
|
container.dispatchEvent(new CustomEvent('error', { detail: err, bubbles: true }))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}, Util.makeDom(/*html*/`
|
2026-05-24 13:23:32 +08:00
|
|
|
|
<div $class="auto-form-root d-flex \${this.inline ? 'auto-form-inline h-100 w-100' : ''}">
|
|
|
|
|
|
<form $class="w-100 d-flex flex-fill \${this.inline ? 'align-items-stretch h-100' : (this.vertical ? 'flex-column' : 'auto-grid-form')}" $onsubmit="event.stopPropagation();event.preventDefault();this.submit()">
|
2026-05-14 20:04:31 +08:00
|
|
|
|
<div $each="this.state.schema || []" style="display:contents">
|
2026-05-17 16:59:44 +08:00
|
|
|
|
<label $if="!this.inline" $name="item.name" class="col-form-label text-muted" $text="item.label"></label>
|
2026-05-23 17:37:24 +08:00
|
|
|
|
<div control-wrapper $class="\${this.inline ? 'flex-grow-1 h-100 d-flex align-items-center' : 'mb-3'}">
|
2026-05-24 13:23:32 +08:00
|
|
|
|
<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 h-100 \${item.type === 'number' ? 'text-end' : ''}">
|
|
|
|
|
|
<select $if="item.type === 'select'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]" class="form-select h-100">
|
2026-05-23 17:37:24 +08:00
|
|
|
|
<option value="" $if="item.placeholder" $text="item.placeholder" disabled></option>
|
|
|
|
|
|
<option $each="item.options" $value="item.value || item" $text="item.label || item"></option>
|
|
|
|
|
|
</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 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">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</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>
|
2026-05-14 20:04:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-17 16:59:44 +08:00
|
|
|
|
<div $if="!this.inline" class="d-flex justify-content-end align-items-baseline gap-3 mt-3" style="grid-column:1/-1">
|
2026-05-14 20:04:31 +08:00
|
|
|
|
<div slot-id="actions"></div>
|
|
|
|
|
|
<button type="submit" class="btn btn-primary" $text="this.submitlabel || '{#Submit#}'"></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
2026-05-17 16:59:44 +08:00
|
|
|
|
`), Util.makeDom(/*html*/`<style>
|
|
|
|
|
|
@media (min-width: 576px) { .auto-grid-form {display: grid;grid-template-columns: max-content 1fr} .auto-grid-form .col-form-label {text-align: right; margin-bottom: 1rem;padding-right: 1rem;max-width: 200px} }
|
|
|
|
|
|
.auto-form-inline { background-color: var(--bs-body-bg); }
|
|
|
|
|
|
.auto-form-inline.min-h-100 { min-height: 100%; height: auto; }
|
|
|
|
|
|
.auto-form-inline .form-control, .auto-form-inline .form-select { border-radius: 0; border: none; background-color: transparent !important; min-height: 100%; height: auto; width: 100%; padding: 0 8px; box-shadow: none; color: inherit; }
|
|
|
|
|
|
.auto-form-inline .form-select { background-position: right 4px center; background-size: 12px 10px; padding-right: 20px; }
|
|
|
|
|
|
.auto-form-inline textarea.form-control { padding: 8px; min-height: 100px; height: auto; }
|
|
|
|
|
|
</style>`))
|
2026-05-14 20:04:31 +08:00
|
|
|
|
|
|
|
|
|
|
const _pendingAutoFormComponents = []
|
|
|
|
|
|
export const AutoForm = {
|
|
|
|
|
|
register: name => {
|
|
|
|
|
|
if (typeof document !== 'undefined') {
|
|
|
|
|
|
if (document.readyState !== 'loading' && Component.getTemplate('AutoForm')) AutoForm._addAutoFormComponent(name)
|
|
|
|
|
|
else _pendingAutoFormComponents.push(name)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
_addAutoFormComponent: name => {
|
|
|
|
|
|
const template = Component.getTemplate('AutoForm')
|
|
|
|
|
|
if (template) {
|
2026-05-23 17:37:24 +08:00
|
|
|
|
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}>`))
|
|
|
|
|
|
}
|
2026-05-17 16:59:44 +08:00
|
|
|
|
}
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}
|
2026-05-23 17:37:24 +08:00
|
|
|
|
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof document !== 'undefined') {
|
2026-05-23 17:37:24 +08:00
|
|
|
|
const initAutoForm = () => {
|
2026-05-14 20:04:31 +08:00
|
|
|
|
_pendingAutoFormComponents.forEach(name => AutoForm._addAutoFormComponent(name))
|
|
|
|
|
|
_pendingAutoFormComponents.length = 0
|
|
|
|
|
|
}
|
2026-05-17 16:59:44 +08:00
|
|
|
|
if (document.readyState !== 'loading') setTimeout(initAutoForm, 100)
|
2026-05-23 17:37:24 +08:00
|
|
|
|
else document.addEventListener('DOMContentLoaded', () => setTimeout(initAutoForm, 100), true)
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Component.register('TagsInput', container => {
|
2026-05-23 17:37:24 +08:00
|
|
|
|
container.state = NewState({ tags: [] })
|
2026-05-14 20:04:31 +08:00
|
|
|
|
container.addEventListener('bind', e => {
|
2026-05-23 17:37:24 +08:00
|
|
|
|
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 : []; }
|
2026-05-14 20:04:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
}, Util.makeDom(/*html*/`
|
2026-05-23 17:37:24 +08:00
|
|
|
|
<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 && this.state.tags) || []" type="button" class="btn btn-sm btn-outline-primary rounded-pill py-0 px-2" $onkeydown="${Util.getFunctionBody(function (event) {
|
2026-05-14 20:04:31 +08:00
|
|
|
|
if (['Backspace', 'Delete'].includes(event.key)) {
|
|
|
|
|
|
event.preventDefault()
|
2026-05-23 17:37:24 +08:00
|
|
|
|
if (this.state?.tags) {
|
|
|
|
|
|
this.state.tags.splice(index, 1)
|
|
|
|
|
|
this.state.tags = this.state.tags
|
|
|
|
|
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: this.state.tags }))
|
|
|
|
|
|
Promise.resolve().then(() => {
|
|
|
|
|
|
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(); }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
})}" $text="item"></button>
|
2026-05-23 17:37:24 +08:00
|
|
|
|
<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) {
|
2026-05-14 20:04:31 +08:00
|
|
|
|
if (event.isComposing) return
|
|
|
|
|
|
if (['Enter', ',', ' '].includes(event.key)) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
const v = thisNode.value.trim()
|
2026-05-23 17:37:24 +08:00
|
|
|
|
if (v && this.state?.tags && !this.state.tags.includes(v)) {
|
2026-05-14 20:04:31 +08:00
|
|
|
|
this.state.tags.push(v)
|
|
|
|
|
|
this.state.tags = this.state.tags
|
2026-05-23 17:37:24 +08:00
|
|
|
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: this.state.tags }))
|
2026-05-14 20:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
thisNode.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})}">
|
|
|
|
|
|
</div>
|
2026-05-23 17:37:24 +08:00
|
|
|
|
`), 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>`))
|
2026-05-14 20:04:31 +08:00
|
|
|
|
|
|
|
|
|
|
AutoForm.register('TagsInput')
|
2026-05-23 17:37:24 +08:00
|
|
|
|
|