import { effect, h } from '@ui-arch/frame'; import type { EditableProps } from './types.js'; /*** 行内编辑:静止态就是文字(hover 才露出可编辑暗示),点一下就地变输入框;Enter 提交、Esc 取消、失焦提交;validate 不过则留在编辑态并标红(与 form 同口径) ***/ export function editable(props: EditableProps): HTMLElement { // —— 状态集中声明 —— const value = props.value; // 唯一数据源:只有提交才写回,取消绝不落值 let editing = false; // 是否处于编辑态 let canceling = false; // Esc 触发的失焦不算提交(blur 让位一次) let painted: string | null = null; // 上次静止态渲染的值;null 表示还没画过(effect 首跑必须画一次) const root = h('div', { class: 'ua-editable' }) as HTMLElement; const isDisabled = (): boolean => (typeof props.disabled === 'function' ? props.disabled() : !!props.disabled); // 布尔与信号两种入参统一判断 /*** 回到静止态并重画(提交 / 取消共用一条出口,避免两条路径各漏一半清理) ***/ const stop = (): void => { editing = false; painted = null; paint(); canceling = false; }; // 画完再复位:paint 里的 replaceChildren 会同步触发 blur,那时 canceling 必须仍为 true /*** 提交:先去空白再校验;校验不过留在编辑态标红,过了才写回信号与回调 ***/ const commit = (raw: string, field: HTMLElement): void => { const next = raw.trim(); const msg = props.validate?.(next) ?? null; if (msg) { field.setAttribute('aria-invalid', 'true'); const tip = root.querySelector('.ua-editable-error'); if (tip) tip.textContent = msg; else root.append(h('span', { class: 'ua-editable-error', role: 'alert' }, msg)); return; } value(next); props.onCommit?.(next); stop(); }; /*** 画一遍:编辑态渲染输入控件并聚焦,静止态渲染整块可点的文字按钮 ***/ const paint = (): void => { if (editing) { const field = props.multiline ? h('textarea', { class: 'ua-editable-input', rows: 3, disabled: props.disabled }, value()) : h('input', { class: 'ua-editable-input', disabled: props.disabled, value: value() }); const el = field as HTMLInputElement | HTMLTextAreaElement; el.addEventListener('keydown', ((e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); canceling = true; stop(); return; } // Esc:丢弃草稿 const enter = e.key === 'Enter' && (!props.multiline || e.ctrlKey || e.metaKey); // 多行只认 Ctrl/Cmd+Enter,换行留给 Enter if (enter) { e.preventDefault(); commit(el.value, el); } }) as EventListener); // 联合类型(input | textarea)取不到同一个重载,这里显式收窄 el.addEventListener('blur', () => { if (canceling) { canceling = false; return; } // 卫语句:取消导致的失焦不提交 commit(el.value, el); }); root.replaceChildren(el); el.focus(); el.select?.(); return; } const text = value(); painted = text; root.replaceChildren(h('button', { type: 'button', class: `ua-editable-view${text ? '' : ' is-empty'}`, disabled: props.disabled, title: text ? '点击编辑' : '点击填写', 'aria-label': `${props.label ?? '编辑'}:${text || '空'}`, onClick: () => { if (isDisabled()) return; editing = true; paint(); }, // 卫语句:禁用态点了也不进编辑 }, text || props.placeholder || '点击填写')); }; effect(() => { const next = value(); if (!editing && next !== painted) paint(); // 外部改值只同步静止态,不打断正在编辑的草稿 }); return root; }