import { h } from '@ui-arch/frame'; import { button } from './button.js'; import { textField } from './textField.js'; import type { FormItemHandle, FormItemProps, FormProps, FormRule } from './types.js'; /*** 执行单条规则:null 通过,字符串为错误消息 ***/ function checkRule(value: string, rule: FormRule): string | null { if (rule.required && !value.trim()) return rule.message ?? '该项为必填项'; if (!value) return null; // 非必填且为空:其余规则跳过 if (rule.min !== undefined && value.length < rule.min) return rule.message ?? `至少 ${rule.min} 个字符`; if (rule.max !== undefined && value.length > rule.max) return rule.message ?? `最多 ${rule.max} 个字符`; if (rule.pattern && !rule.pattern.test(value)) return rule.message ?? '格式不正确'; if (rule.validator) { const r = rule.validator(value); if (r !== true) return typeof r === 'string' ? r : rule.message ?? '校验未通过'; } return null; } /*** 表单字段:label + 控件插槽 + 错误区;返回句柄供 form 聚合校验 ***/ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHandle { // —— 状态集中声明 —— const mode = props.validateOn ?? 'blur'; // 校验时机;缺省失焦校验 const rules = [...(props.required ? [{ required: true }] : []), ...(props.rules ?? [])]; const error = h('div', { class: 'ua-form-error' }); const label = h('label', { class: 'ua-form-label' }, props.label, props.required ? h('i', { class: 'ua-form-req' }, '*') : null); const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, error)); /*** 错误态落点:控件根 + 内部原生控件,保证 select / checkbox 这类包裹式控件也能标红 ***/ const marked = (): HTMLElement[] => [ control, ...Array.from(control.querySelectorAll('input, textarea, select, .ua-select-trigger')), ]; /*** 执行全部规则:返回首个错误消息,同时同步错误区、控件态与 aria-invalid ***/ const validate = () => { const value = props.value(); let msg: string | null = null; for (const rule of rules) { msg = checkRule(value, rule); if (msg) break; // 首个失败即停止 } error.textContent = msg ?? ''; control.classList.toggle('ua-invalid', !!msg); for (const node of marked()) node.setAttribute('aria-invalid', String(!!msg)); // 读屏同步错误态 return msg; }; control.addEventListener('input', () => { if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误 }); control.addEventListener('change', () => { if (mode === 'input') validate(); // select / checkbox / radio 变更即校验(原生 input 事件不冒泡) }); control.addEventListener('focusout', () => { if (mode !== 'submit') validate(); // 用会冒泡的 focusout 取代 blur,包裹式控件同样生效 }); return { el, validate }; } /*** 表单容器:布局 + 原生 submit 拦截 + 聚合校验(全部通过才触发 onSubmit) ***/ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement { // —— 状态集中声明 —— /*** 整体提交:逐项校验,聚焦首个出错项并中止 ***/ const submit = () => { for (const item of items) { if (item.validate()) { (item.el.querySelector('input, textarea, select') as HTMLElement | null)?.focus(); return; // 卫语句:有错误不提交 } } props.onSubmit?.(); }; const submitBtn = button({ label: props.submitText ?? '提交', onClick: submit }); const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', onClick: props.onReset }) : null; const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn); const el = h( 'form', { class: props.layout === 'horizontal' ? 'ua-form ua-form-h' : 'ua-form' }, ...items.map((i) => i.el), actions, ); el.addEventListener('submit', (e) => { e.preventDefault(); // 拦截原生提交,走校验流程 submit(); }); return el; }