iter(g244): 新增 inputTag 标签输入组件,回车成标签并支持满额禁输
This commit is contained in:
@@ -75,3 +75,4 @@ export { copyButton } from './copyButton.js';
|
||||
export { countdown } from './countdown.js';
|
||||
export { kbd } from './kbd.js';
|
||||
export { highlight } from './highlight.js';
|
||||
export { inputTag } from './inputTag.js';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { effect, h, type Signal } from '@ui-arch/frame';
|
||||
import type { InputTagProps } from './types.js';
|
||||
|
||||
/*** 作用:把布尔或信号口径的禁用态读成当前值(供渲染时统一判断,天然跟随信号) ***/
|
||||
const off = (v: boolean | Signal<boolean> | undefined): boolean => (typeof v === 'function' ? v() : Boolean(v));
|
||||
|
||||
/*** 作用:标签输入——回车 / 逗号成标签、chip 逐个删、空框退格删末个、失焦自动入列;外框与 chip 复用现有类,零新增视觉字面量 ***/
|
||||
export function inputTag(props: InputTagProps): HTMLElement {
|
||||
const root = h('div', { class: 'ua-input-group ua-input-tag' }) as HTMLElement;
|
||||
const hint = props.placeholder ?? '输入后回车添加'; // 未满时的占位文案
|
||||
const input = h('input', { class: 'ua-input', placeholder: hint, 'aria-label': props.ariaLabel ?? '标签输入' }) as HTMLInputElement;
|
||||
const limit = props.max ?? 0; // 0 = 不限条数
|
||||
const maxLength = props.maxLength ?? 24; // 单个标签超长直接截断,防止一个标签撑爆整行
|
||||
const allowDuplicate = props.allowDuplicate ?? false;
|
||||
root.append(input);
|
||||
|
||||
/*** 作用:写回标签集合(单一出口:信号与 onChange 永远同源,避免两处各写一次) ***/
|
||||
const put = (next: string[]): void => {
|
||||
props.value(next);
|
||||
props.onChange?.(next);
|
||||
};
|
||||
|
||||
/*** 作用:追加一个标签:截断超长、丢弃重复与超限,并把输入框清空 ***/
|
||||
const commit = (raw: string): void => {
|
||||
const text = raw.trim().slice(0, maxLength);
|
||||
input.value = '';
|
||||
if (!text) return;
|
||||
const list = props.value();
|
||||
if (limit > 0 && list.length >= limit) return; // 已达上限:静默丢弃,输入框已清空
|
||||
if (!allowDuplicate && list.includes(text)) return; // 重复标签:不重复入列
|
||||
put([...list, text]);
|
||||
};
|
||||
|
||||
/*** 作用:按信号重画 chip 与输入框状态(chip 全量重建,input 始终留在末尾) ***/
|
||||
const render = (): void => {
|
||||
const list = props.value();
|
||||
root.querySelectorAll('.ua-tag').forEach((node) => node.remove()); // 先清后建:条数少,重建比 diff 稳
|
||||
for (const tag of list) {
|
||||
const close = h('button', {
|
||||
type: 'button',
|
||||
class: 'ua-tag-close',
|
||||
'aria-label': `移除标签 ${tag}`,
|
||||
onClick: () => put(props.value().filter((item) => item !== tag)), // 按值删,不用索引(外部可能已改序)
|
||||
}, '×');
|
||||
root.insertBefore(h('span', { class: 'ua-tag' }, h('span', {}, tag), close), input);
|
||||
}
|
||||
const full = limit > 0 && list.length >= limit;
|
||||
input.disabled = off(props.disabled) || full; // 满额即禁输:占位文案同时改成「已达上限」
|
||||
input.placeholder = full ? `最多 ${limit} 个标签` : hint;
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ',' || event.key === ',') {
|
||||
event.preventDefault(); // 回车不提交外层表单;逗号不留进输入值
|
||||
commit(input.value);
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Backspace' && input.value === '') {
|
||||
const list = props.value();
|
||||
if (list.length) put(list.slice(0, -1)); // 空框退格:删掉最后一个 chip
|
||||
}
|
||||
});
|
||||
input.addEventListener('blur', () => commit(input.value)); // 失焦即入列:手输后忘了回车也不会丢
|
||||
root.addEventListener('click', () => input.focus()); // 点外框任意位置都聚焦输入框
|
||||
|
||||
effect(render);
|
||||
return root;
|
||||
}
|
||||
@@ -985,3 +985,15 @@ export interface CommandPaletteHandle {
|
||||
open: () => void; // 打开:清空搜索词并聚焦输入框
|
||||
close: () => void; // 关闭:归还打开前焦点
|
||||
}
|
||||
|
||||
/*** 标签输入入参 ***/
|
||||
export interface InputTagProps {
|
||||
value: Signal<string[]>; // 已选标签集合,双向绑定(成标签 / 删 chip 都写回)
|
||||
placeholder?: string; // 未满时的占位文案;缺省「输入后回车添加」
|
||||
max?: number; // 最多允许的标签条数;缺省 0 表示不限
|
||||
maxLength?: number; // 单个标签最大字符数;缺省 24(超出截断)
|
||||
allowDuplicate?: boolean; // 是否允许重复标签;缺省 false
|
||||
disabled?: boolean | Signal<boolean>; // 禁用态:布尔或信号,满额时也会禁用输入
|
||||
ariaLabel?: string; // 输入框无障碍标签;缺省「标签输入」
|
||||
onChange?: (tags: string[]) => void; // 标签集合变化回调(参数是变化后的完整集合)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@
|
||||
"copyButton(props):复制按钮,点击把 text 写入剪贴板并短暂显示已复制,text 可传函数取实时值,clipboard API 不可用时降级 execCommand",
|
||||
"countdown(props):倒计时,秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示,返回句柄 start/stop/reset",
|
||||
"kbd(props):键盘按键标记,键帽样式展示单个按键或组合键(多键以 + 连接),用于快捷键提示",
|
||||
"highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新"
|
||||
"highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新",
|
||||
"inputTag(props):标签输入,输入后回车 / 逗号成标签、chip 可单个删除、空框退格删末个,value 信号双向绑定,max 满额自动禁输"
|
||||
],
|
||||
"keyTypes": [
|
||||
"ButtonProps",
|
||||
@@ -3898,6 +3899,69 @@
|
||||
"两个入参都接受信号:搜索框输入实时联动高亮,无需手动重渲染",
|
||||
"大小写不敏感且全部命中(非只第一处);关键词两侧空白自动忽略"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inputTag",
|
||||
"title": "标签输入 InputTag",
|
||||
"demo": "sec-inputtag",
|
||||
"props": [
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string[]>",
|
||||
"r": 1,
|
||||
"d": "已选标签集合,双向绑定:成标签与删 chip 都写回信号"
|
||||
},
|
||||
{
|
||||
"n": "placeholder",
|
||||
"t": "string",
|
||||
"r": 0,
|
||||
"d": "未满时的占位文案;缺省「输入后回车添加」"
|
||||
},
|
||||
{
|
||||
"n": "max",
|
||||
"t": "number",
|
||||
"r": 0,
|
||||
"d": "最多允许的标签条数;缺省 0 表示不限,满额后输入框禁用并提示上限"
|
||||
},
|
||||
{
|
||||
"n": "maxLength",
|
||||
"t": "number",
|
||||
"r": 0,
|
||||
"d": "单个标签最大字符数;缺省 24,超出直接截断"
|
||||
},
|
||||
{
|
||||
"n": "allowDuplicate",
|
||||
"t": "boolean",
|
||||
"r": 0,
|
||||
"d": "是否允许重复标签;缺省 false(重复的静默丢弃)"
|
||||
},
|
||||
{
|
||||
"n": "disabled",
|
||||
"t": "boolean | Signal<boolean>",
|
||||
"r": 0,
|
||||
"d": "禁用态:布尔或信号,true 时不可输入也不可删"
|
||||
},
|
||||
{
|
||||
"n": "ariaLabel",
|
||||
"t": "string",
|
||||
"r": 0,
|
||||
"d": "输入框无障碍标签;缺省「标签输入」"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(tags: string[]) => void",
|
||||
"r": 0,
|
||||
"d": "标签集合变化回调,参数是变化后的完整集合"
|
||||
}
|
||||
],
|
||||
"example": "const tags = signal(['前端', '设计']);\ninputTag({ value: tags, max: 5, onChange: (list) => console.log(list.length) })",
|
||||
"steps": [
|
||||
"传一个 signal<string[]> 作为 value:回车或逗号把当前输入转成 chip,写回信号后立即重画",
|
||||
"点 chip 上的 × 删除单个标签;输入框为空时按退格键删掉最后一个(手不离键盘也能改)",
|
||||
"输入后直接失焦也会成标签,不必强行回车;max 满额后输入框禁用,占位文案变成「最多 N 个标签」",
|
||||
"要区分大小写重复判断就传 allowDuplicate: true;默认按原样去重",
|
||||
"样式复用输入组与标签:外框聚焦光环、chip 圆角与语义色都跟既有组件一致,无新增视觉字面量"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5259,3 +5259,27 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
|
||||
animation: none; /* 降动效:转圈静态化为实色环 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 标签输入:外框复用输入组(边框 / 聚焦光环),chip 复用 .ua-tag,这里只排布 ----
|
||||
多标签必须换行:单行 flex 会把 chip 挤成省略号,用户看不出已选几个 */
|
||||
.ua-input-tag {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ua-space-1);
|
||||
padding: var(--ua-space-1) var(--ua-space-2);
|
||||
min-height: var(--ua-control-h-md);
|
||||
cursor: text; /* 点框内任意处都能输入 */
|
||||
}
|
||||
.ua-input-tag .ua-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; /* 允许收缩:否则长 chip 会把输入框顶出容器 */
|
||||
padding: var(--ua-space-05) 0; /* 纵向内距交给外框,输入框自带内距会让 chip 与文字错行 */
|
||||
height: auto;
|
||||
}
|
||||
.ua-input-tag .ua-input:disabled {
|
||||
background: transparent; /* 满额 / 禁用时外框仍显示 chip,内层不叠一层灰底 */
|
||||
}
|
||||
.ua-input-tag .ua-tag {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user