@zerodep-dev/vue-tsx-pro-ui 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/display/VirtualList.d.ts +1 -2
- package/dist/display/VirtualList.js +11 -21
- package/dist/display/VirtualList.js.map +1 -1
- package/dist/input/Combobox.d.ts +1 -1
- package/dist/input/Combobox.js.map +1 -1
- package/dist/input/ListPopover.d.ts +1 -1
- package/dist/input/ListPopover.js.map +1 -1
- package/dist/input/Listbox.d.ts +1 -2
- package/dist/input/Listbox.js +7 -19
- package/dist/input/Listbox.js.map +1 -1
- package/dist/input/Select.d.ts +1 -1
- package/dist/input/Select.js.map +1 -1
- package/dist/overlay/CommandPalette.d.ts +1 -1
- package/dist/overlay/CommandPalette.js.map +1 -1
- package/dist/overlay/detached.js +14 -1
- package/dist/overlay/detached.js.map +1 -1
- package/dist/patterns/QueryTable.js +0 -2
- package/dist/patterns/QueryTable.js.map +1 -1
- package/dist/table/DataTable.js +1 -1
- package/dist/table/DataTable.js.map +1 -1
- package/dist/table/DataTableView.d.ts +0 -3
- package/dist/table/DataTableView.js +32 -26
- package/dist/table/DataTableView.js.map +1 -1
- package/dist/table/table-styles.d.ts +1 -1
- package/dist/table/table-styles.js +0 -13
- package/dist/table/table-styles.js.map +1 -1
- package/package.json +2 -2
package/dist/input/Select.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.js","names":["_vtp_readonlyProp","_vtp_bindable","_vtp_childrenProp","_vtp_restProps","_vue_shallowRef","_createVNode","_Fragment","_mergeProps"],"sources":["../../src/input/Select.tsx"],"sourcesContent":["/**\n * 下拉选择(WAI-ARIA 的 select-only combobox):点触发框弹出选项列表,单选或多选。\n *\n * <Field label=\"城市\"><Select items={['北京', '上海', '广州']} _value={city} placeholder=\"选择城市\" /></Field>\n * <Select items={tags} selectionMode=\"multiple\" _value={picked} clearable aria-label=\"标签\" />\n * <Select items={users} toOption={(user) => ({ value: user.id, label: user.name })} _value={id} />\n * <Select items={suppliers} searchable _value={supplier} /> 选项多时:弹出层顶部带搜索框\n *\n * <Select items={tags} selectionMode=\"multiple\" maxTagCount=\"responsive\" creatable onCreate={(text) => tags.push(text)} … />\n *\n * 组合已有的部件:外壳是输入框外壳(controlParts),弹出层用 useAnchoredPopover(与外壳同宽、最高到视口剩余空间),\n * 列表是 ListboxView(状态与键盘见 list-state.ts)。焦点一直在触发按钮上,高亮项经 aria-activedescendant 告诉读屏。\n *\n * 多选时已选的项是触发按钮前面的一排 Tag(不能放进 button 里:按钮里不能有按钮),各带移除按钮。\n * 这排标签只给指针用(aria-hidden、移除按钮不进 Tab 顺序):读屏从触发按钮里念出全部已选内容,\n * 键盘在列表里取消勾选,或在触发按钮(及空的搜索框)里按 Backspace 移除最后一项(同 TagsInput)。\n * maxTagCount 是数字时只显示前几个,'responsive' 时排成一行按宽度放,其余收进“+N”(SelectedTags,TreeSelect 共用)。\n *\n * 键盘:关闭时 ↓ / Enter / 空格打开(↑ 打开并高亮最后一项),单选时直接按字母选中以它开头的项(同原生 select);\n * 打开时 ↑↓、Home / End、Page Up / Down 移动,Enter / 空格选中(单选随后关闭),Esc、Tab 关闭。\n * 写了 name 时用隐藏的 input 随表单提交(多选时每个值一个)。\n *\n * searchable 时弹出层顶部有搜索框:打开后焦点在搜索框里,输入就过滤选项(按显示文字,忽略大小写与重音,或用 filter),\n * ↑↓ 移动、Enter 选中、Esc 关闭并把焦点还给触发按钮。已选的项被过滤掉、或 items 换了(远程搜索)时,\n * 触发框里仍显示它的文字(useSelectedOptions)。\n *\n * 远程选项:给 load 时打开就按搜索词请求,输入停下 300 ms 再请求,只采用最后一次(remote-items.ts);\n * 自己管请求时写 _search={keyword} 拿到搜索词、filter={false} 关掉本地过滤,items 与 loading 自己给。\n *\n * <Select searchable load={(q, signal) => http.get<User[]>('/users', { query: { q }, signal })} toOption={…} _value={owner} />\n *\n * creatable 时自带搜索框,输入的文字没有对应的选项时列表末尾多一项“新建“…””:选中后值就是这段文字,onCreate 报告它。\n *\n * 部件:Root(外壳)、Tags(多选时的那排标签)、Tag、Trigger(button,主元素)、Value、Clear、Chevron、Popover、\n * Search(TextField)、Listbox。\n */\nimport { ChevronDown, Plus, Search } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { useId, watch, type VNodeChild } from 'vue';\nimport type { PresetIconButtonProps } from '../base/CloseButton';\nimport { buttonBaseStyles } from '../base/button-core';\nimport { Icon, type IconProps } from '../base/Icon';\nimport type { TagProps } from '../base/Tag';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { useLang, useTextMatcher } from '../config/context';\nimport { useAnchoredPopover, type FloatingPlacement } from '../utils/dom/floating';\nimport { openSetter, type OpenChangeProps } from '../utils/dom/open-change';\nimport { ListPopover } from './ListPopover';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { selectedValues, type SelectionProps } from '../utils/data/selection';\nimport {\n controlClear,\n controlParts,\n clickOnShell,\n focusOnShell,\n controlIcon,\n useControlAppearance,\n type ControlIconOptions,\n type ControlOptions,\n} from './control';\nimport { useFieldControl } from '../form/Field';\nimport { FormValue } from '../form/FormValue';\nimport { disclosureStyles } from '../utils/style/transitions';\nimport { activeOptionId, type ListboxViewProps, type OptionRenderState } from './Listbox';\nimport {\n handleListKeydown,\n useListState,\n useResultAnnouncement,\n useSelectedOptions,\n type ListboxOption,\n type OptionMapping,\n toListboxOption,\n} from './list-state';\nimport { useRemoteItems, type OptionsLoader } from './remote-items';\nimport { SelectedTags, type SelectedTagsProps } from './SelectedTags';\nimport { TextField, type TextFieldProps } from './TextField';\n\n/** 角色、展开状态与可用性由组件管理 */\nexport type SelectManaged =\n | 'type'\n | 'role'\n | 'value'\n | 'disabled'\n | 'aria-haspopup'\n | 'aria-expanded'\n | 'aria-controls'\n | 'aria-activedescendant'\n | 'aria-invalid'\n | 'aria-required'\n | 'aria-describedby';\n\n/** 下拉选择共有的属性;未声明的属性落到触发按钮(主元素)上,外壳用 propsRoot */\nexport interface SelectBaseProps<T>\n extends PartProps<'button', SelectManaged>, ControlOptions, ControlIconOptions, OpenChangeProps {\n /** 数据:ListboxOption、字符串,或任意数据加 toOption;有 load 时是第一次加载之前的选项(编辑页放上已选的那一项) */\n items?: readonly T[];\n /** 没有选择时显示的文字,默认取语言包(请选择) */\n placeholder?: string;\n /** 是否打开:父级可以写 _open={x} */\n _open?: boolean;\n /** 弹出位置,默认 bottom-start */\n placement?: FloatingPlacement;\n /** 有选择时显示清空按钮 */\n clearable?: boolean;\n /** 弹出层顶部带搜索框,输入过滤选项:选项多(供应商、人员、件号)时用 */\n searchable?: boolean;\n /** 搜索时怎样算匹配;默认显示文字包含输入(忽略大小写与重音)。false 时不在本地过滤(服务端按 search 过滤) */\n filter?: false | ((item: T, query: string) => boolean);\n /** 搜索框里的文字:打开时清空;父级写 _search={x} 拿到它(自己发请求时) */\n _search?: string;\n /** 远程选项:打开时与搜索词停下 300 ms 后调用,结果代替 items;只采用最后一次,被顶掉的请求 signal 中止 */\n load?: OptionsLoader<T>;\n /**\n * 可以新建选项(自带搜索框):输入的文字没有对应的选项时,列表末尾多一项“新建“…””,选中后值就是这段文字。\n * 在 onCreate 里把它加进 items(或存到服务端);不加时触发框里照样显示这段文字\n */\n creatable?: boolean;\n /** 新建了一项(creatable 时):参数是输入的文字,也就是选中的值 */\n onCreate?: (text: string) => void;\n /** 多选时触发框里最多显示几个标签,其余收进“+N”;'responsive' 时排成一行、按宽度放,放不下的收起 */\n maxTagCount?: number | 'responsive';\n /** 表单提交时的 name(用隐藏的 input) */\n name?: string;\n /** 不可用 */\n disabled?: boolean;\n /** 只读:能聚焦、能看到已选的内容,不能打开、不能清空 */\n readonly?: boolean;\n /** 必填 */\n required?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 自定义选项内容:`{(option, { selected, active }) => …}` */\n children?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 自定义触发框里显示的内容(已选的选项);默认单选显示文字、多选显示标签 */\n renderValue?: (selected: readonly T[]) => VNodeChild;\n /** 正在取选项(异步加载):没有选项时列表里是骨架行,已有选项时顶部显示进度条 */\n loading?: boolean;\n /** 没有选项时列表里显示的内容 */\n empty?: VNodeChild;\n /** 外壳的部件属性 */\n propsRoot?: PartProps<'div'>;\n /** 已选内容那一层的部件属性 */\n propsValue?: PartProps<'span'>;\n /** 多选时每个标签的部件属性 */\n propsTag?: Partial<TagProps>;\n /** 多选时那排标签的部件属性 */\n propsTags?: Partial<SelectedTagsProps>;\n /** 清空按钮的部件属性 */\n propsClear?: Partial<PresetIconButtonProps>;\n /** 下拉箭头的部件属性 */\n propsChevron?: Partial<IconProps>;\n /** 弹出层的部件属性 */\n propsPopover?: PartProps<'div', 'id' | 'popover'>;\n /** 列表的部件属性 */\n propsListbox?: Partial<ListboxViewProps<T>>;\n /** 搜索框的部件属性(searchable 时) */\n propsSearch?: Partial<TextFieldProps>;\n}\n\n/** 单选或多选;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type SelectProps<T = ListboxOption> = SelectBaseProps<T> & SelectionProps & OptionMapping<T>;\n\n/** Select 的样式:root、trigger、value、placeholder、chevronOpen 等 */\nexport const selectStyles = css.create({\n // 在 ControlGroup 里也按内容宽(输入框才分剩余宽度)\n root: { cursor: 'pointer', flexGrow: 0 },\n // 去掉按钮默认外观用 buttonBaseStyles;像输入框一样没有自己的焦点环与波纹(焦点环画在外壳上)\n trigger: {\n display: 'flex',\n alignItems: 'center',\n flexGrow: 1,\n minWidth: 0,\n alignSelf: 'stretch',\n textAlign: 'start',\n cursor: 'inherit',\n outlineStyle: 'none',\n },\n value: {\n display: 'flex',\n flexWrap: 'wrap',\n alignItems: 'center',\n gap: '_xs',\n minWidth: 0,\n paddingBlock: '_xs',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n },\n // 是真正的文字(不是原生 placeholder),要满足 4.5:1:用 textMuted 而不是 textPlaceholder\n placeholder: { color: '_textMuted' },\n // 多选时标签排在触发按钮前面(SelectedTags)占剩余宽度,触发按钮里只剩读屏用的文字\n triggerBehindTags: { flexGrow: 0 },\n chevronOpen: { rotate: '180deg' },\n // 弹出层顶部的搜索框占满宽度\n search: { width: '_full' },\n});\n\n/** creatable 时列表末尾那一项“新建“…””:不是 items 里的数据 */\nclass Creation {\n readonly text: string;\n constructor(text: string) {\n this.text = text;\n }\n}\n\n/** 下拉选择:触发框 + 弹出的选项列表 */\nexport const Select = _component(\n <T,>({\n items = [],\n toOption,\n selectionMode = 'single',\n _value: value = _bindable<string | string[] | null>(null),\n _open: open = _bindable(false),\n onOpenChange,\n placeholder,\n placement = 'bottom-start',\n clearable = false,\n searchable = false,\n filter,\n _search: search = _bindable(''),\n load,\n creatable = false,\n onCreate,\n maxTagCount,\n name,\n variant,\n size,\n radius,\n disabled,\n readonly,\n required,\n invalid,\n id,\n icon,\n children,\n renderValue,\n empty,\n loading = false,\n propsRoot,\n propsIcon,\n propsValue,\n propsTag,\n propsTags,\n propsClear,\n propsChevron,\n propsPopover,\n propsListbox,\n propsSearch,\n ...rest\n }: SelectProps<T>) => {\n const lang = useLang();\n const textMatcher = useTextMatcher();\n const listId = useId();\n const appearance = useControlAppearance(() => ({ variant, size, radius }));\n const control = useFieldControl(() => ({ id, invalid, required, disabled, readonly }));\n const multiple = () => selectionMode === 'multiple';\n const optionOf = (item: T) => toListboxOption(item, toOption);\n const remote = useRemoteItems({\n load: () => load,\n query: () => search,\n open: () => open,\n items: () => items,\n });\n // 本地过滤:有 load 或 filter={false} 时交给服务端\n const localFilter = () => !load && filter !== false;\n const matches = (item: T, text: string) =>\n filter ? filter(item, text) : textMatcher()(optionOf(item).label, text);\n // creatable 自带搜索框\n const withSearch = () => searchable || creatable;\n // creatable:输入的文字没有同名(或同值)的选项时,列表末尾多一项“新建”\n const creation = () => {\n const text = search.trim();\n if (!creatable || !text) return undefined;\n const exists = remote.items().some((item) => {\n const option = optionOf(item);\n return option.value === text || option.label.toLowerCase() === text.toLowerCase();\n });\n return exists ? undefined : new Creation(text);\n };\n const list = useListState<T | Creation>({\n items: () => {\n const all = remote.items();\n const shown =\n withSearch() && search && localFilter()\n ? all.filter((item) => matches(item, search))\n : all;\n const created = creation();\n return created ? [...shown, created] : shown;\n },\n toOption: (item) =>\n item instanceof Creation\n ? { value: item.text, label: lang.selectCreate(item.text), icon: Plus }\n : optionOf(item),\n selected: () => selectedValues(value),\n mode: () => selectionMode,\n onChange: (next) => {\n const created = creation();\n if (created && next.includes(created.text)) {\n onCreate?.(created.text);\n search = '';\n }\n value = multiple() ? [...next] : (next[0] ?? null);\n },\n // 单选时再点已选的一项不取消(同原生 select);要清空用清除按钮\n required: () => !multiple(),\n });\n // 已选项的文字取自全部 items(过滤掉的、远程搜索换掉的也在)\n const selectedOptions = useSelectedOptions({ items: remote.items, toOption: optionOf });\n let shell = _state.raw<HTMLElement | null>(null);\n let trigger = _state.raw<HTMLElement | null>(null);\n let panel = _state.raw<HTMLElement | null>(null);\n const setOpen = openSetter(\n () => open,\n (next) => (open = next),\n () => onOpenChange,\n );\n\n /** 多选时移除一项(标签的移除按钮、Backspace) */\n function remove(target: string) {\n value = selectedValues(value).filter((item) => item !== target);\n }\n\n function removeLast() {\n const last = selectedValues(value).at(-1);\n if (last !== undefined) remove(last);\n }\n\n function show(highlight: 'default' | 'last' = 'default', event?: Event) {\n search = '';\n list.activateDefault();\n if (highlight === 'last' && selectedValues(value).length === 0) list.move('last');\n setOpen(true, 'trigger', event);\n }\n\n useResultAnnouncement(() => ({ query: search, count: list.options.length }));\n\n // 输入搜索词:高亮第一个匹配的。打开时清空搜索词不算:那时高亮的是已选项\n watch(\n () => search,\n (text) => text && list.move('first'),\n );\n\n // 选中一项:单选时关闭\n function picked(event?: Event) {\n if (!multiple()) setOpen(false, 'select', event);\n }\n\n /** Ctrl / Cmd / Alt 组合键是浏览器的(刷新、复制、查找),不做按字母跳转 */\n const hasModifier = (event: KeyboardEvent) => event.altKey || event.ctrlKey || event.metaKey;\n\n function onKeydown(event: KeyboardEvent) {\n if (control().disabled || control().readonly) return;\n if (event.key === 'Backspace' && multiple()) {\n event.preventDefault();\n removeLast();\n return;\n }\n if (!open) {\n if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n show('default', event);\n } else if (event.key === 'ArrowUp') {\n event.preventDefault();\n show('last', event);\n } else if (!multiple() && !hasModifier(event) && list.typeahead(event.key)) {\n // 同原生 select:关闭时按字母直接选中\n event.preventDefault();\n if (list.active !== undefined && !list.isSelected(list.active)) list.select(list.active);\n }\n return;\n }\n if (event.key === 'Tab') {\n setOpen(false, 'focus', event);\n return;\n }\n if (handleListKeydown(list, event, { space: true, typeahead: true }, () => picked(event))) {\n event.preventDefault();\n }\n }\n\n // 搜索框里:↑↓ 移动、Enter 选中(Home / End、空格留给输入),Tab 关闭后照常移到下一个;\n // 多选时空的搜索框里按 Backspace 移除最后一项\n function onSearchKeydown(event: KeyboardEvent) {\n if (event.key === 'Tab') {\n setOpen(false, 'focus', event);\n return;\n }\n if (event.key === 'Backspace' && !search && multiple()) {\n event.preventDefault();\n removeLast();\n return;\n }\n if (handleListKeydown(list, event, { homeEnd: false }, () => picked(event)))\n event.preventDefault();\n }\n\n function clear() {\n value = multiple() ? [] : null;\n trigger?.focus();\n }\n\n // 弹层贴着外壳、与它同宽;焦点一直在触发按钮上,关闭时不用归还(searchable 时见 onPanelToggle)\n const { onToggle } = useAnchoredPopover(\n () => ({\n open,\n anchor: shell,\n panel,\n floating: { placement, matchWidth: true, fitHeight: true },\n restoreFocus: false,\n }),\n { close: (reason, event) => setOpen(false, reason, event) },\n );\n\n // searchable:打开后焦点进搜索框;关闭时焦点还在弹层里(Esc、选中、点外面)就还给触发按钮\n function onPanelToggle(event: ToggleEvent) {\n onToggle(event);\n if (!withSearch()) return;\n if (event.newState === 'open') {\n panel?.querySelector<HTMLInputElement>('input[type=\"search\"]')?.focus();\n return;\n }\n const active = document.activeElement;\n if (!active || active === document.body || panel?.contains(active)) trigger?.focus();\n }\n\n return () => {\n const state = control();\n const values = selectedValues(value);\n const selected = selectedOptions(values);\n const off = state.disabled;\n // 只读:能聚焦,不能打开、不能改\n const locked = off || state.readonly;\n // 多选的默认显示:触发按钮前面一排标签,按钮里只留给读屏的文字\n const tagged = multiple() && !renderValue && selected.length > 0;\n const shown = tagged ? (\n <span {...css.attrs(visuallyHiddenStyles.hidden)}>\n {lang.selectSummary(selected.map((option) => option.label))}\n </span>\n ) : (\n (renderValue?.(\n selected.flatMap((option) => (option.item === undefined ? [] : [option.item])),\n ) ??\n (selected.length === 0 ? (\n <span {...css.attrs(selectStyles.placeholder)}>\n {placeholder ?? lang.selectPlaceholder}\n </span>\n ) : (\n selected[0]!.label\n )))\n );\n const tags = tagged && (\n <SelectedTags\n {...mergePartProps<SelectedTagsProps>(\n {\n tags: selected,\n maxTagCount,\n onRemove: locked ? undefined : remove,\n propsTag,\n },\n propsTags,\n )}\n />\n );\n const s = controlParts(appearance(), state);\n return (\n <>\n <div\n {...mergeAttrs<'div'>({ ...s.root, sx: [s.root.sx, selectStyles.root] }, propsRoot, {\n ref: _toRef(shell),\n // 点在外壳空白处(含多选的标签):焦点交给触发按钮,并与点触发按钮一样切换打开(只读时只聚焦)\n onPointerdown: off ? undefined : focusOnShell(() => trigger),\n onClick: locked\n ? undefined\n : clickOnShell(\n () => trigger,\n (event) => (open ? setOpen(false, 'trigger', event) : show('default', event)),\n ),\n })}\n data-state={open ? 'open' : 'closed'}\n data-invalid={state.invalid || undefined}\n data-disabled={off || undefined}\n >\n {controlIcon(s, icon, propsIcon)}\n {tags}\n <button\n {...mergeAttrs<'button'>(\n {\n sx: [\n buttonBaseStyles.root,\n selectStyles.trigger,\n tagged && selectStyles.triggerBehindTags,\n ],\n },\n rest,\n {\n ref: _toRef(trigger),\n id: state.id,\n type: 'button',\n role: 'combobox',\n disabled: off,\n 'aria-haspopup': 'listbox',\n 'aria-expanded': open,\n 'aria-controls': listId,\n 'aria-activedescendant': open ? activeOptionId(list, listId) : undefined,\n 'aria-invalid': state.invalid || undefined,\n 'aria-required': state.required || undefined,\n 'aria-readonly': state.readonly || undefined,\n 'aria-describedby': state.describedBy,\n onClick: (event: MouseEvent) => {\n // Safari 点击按钮不会聚焦它:焦点要在触发按钮上,键盘与 aria-activedescendant 才有效\n trigger?.focus();\n if (locked) return;\n if (open) setOpen(false, 'trigger', event);\n else show('default', event);\n },\n onKeydown,\n },\n )}\n >\n <span {...mergeAttrs<'span'>({ sx: selectStyles.value }, propsValue)}>{shown}</span>\n </button>\n {clearable && values.length > 0 && !locked && controlClear(s, propsClear, clear)}\n <Icon\n {...mergePartProps<IconProps>(\n {\n ...s.icon,\n icon: ChevronDown,\n sx: [s.icon.sx, disclosureStyles.icon, open && selectStyles.chevronOpen],\n },\n propsChevron,\n )}\n />\n <FormValue name={name} value={values} disabled={state.disabled} />\n </div>\n <ListPopover\n panelRef={_toRef(panel)}\n onToggle={onPanelToggle}\n size={appearance().size}\n labelledBy={state.labelledBy}\n label={rest['aria-label']}\n loading={loading || remote.loading()}\n onPick={() => picked()}\n header={\n withSearch() && (\n <TextField\n {...mergePartProps<TextFieldProps>(\n {\n icon: Search,\n variant: 'soft',\n placeholder: lang.selectSearch,\n 'aria-label': lang.selectSearch,\n propsRoot: { sx: selectStyles.search },\n },\n propsSearch,\n )}\n type=\"search\"\n _value={search}\n aria-controls={listId}\n aria-activedescendant={open ? activeOptionId(list, listId) : undefined}\n onKeydown={onSearchKeydown}\n />\n )\n }\n listbox={{\n list,\n id: listId,\n empty: remote.failure() ?? (search ? lang.selectNoMatch : empty),\n renderOption:\n children &&\n ((item: T | Creation, state: OptionRenderState) =>\n item instanceof Creation ? lang.selectCreate(item.text) : children(item, state)),\n multiple: multiple(),\n }}\n // 使用方按 T 写列表的部件属性;creatable 时多出的“新建”那一项由上面的 renderOption 处理\n propsListbox={propsListbox as Partial<ListboxViewProps<T | Creation>> | undefined}\n propsPopover={propsPopover}\n />\n </>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Select.js","names":["_vtp_readonlyProp","_vtp_bindable","_vtp_childrenProp","_vtp_restProps","_vue_shallowRef","_createVNode","_Fragment","_mergeProps"],"sources":["../../src/input/Select.tsx"],"sourcesContent":["/**\n * 下拉选择(WAI-ARIA 的 select-only combobox):点触发框弹出选项列表,单选或多选。\n *\n * <Field label=\"城市\"><Select items={['北京', '上海', '广州']} _value={city} placeholder=\"选择城市\" /></Field>\n * <Select items={tags} selectionMode=\"multiple\" _value={picked} clearable aria-label=\"标签\" />\n * <Select items={users} toOption={(user) => ({ value: user.id, label: user.name })} _value={id} />\n * <Select items={suppliers} searchable _value={supplier} /> 选项多时:弹出层顶部带搜索框\n *\n * <Select items={tags} selectionMode=\"multiple\" maxTagCount=\"responsive\" creatable onCreate={(text) => tags.push(text)} … />\n *\n * 组合已有的部件:外壳是输入框外壳(controlParts),弹出层用 useAnchoredPopover(与外壳同宽、最高到视口剩余空间),\n * 列表是 ListboxView(状态与键盘见 list-state.ts)。焦点一直在触发按钮上,高亮项经 aria-activedescendant 告诉读屏。\n *\n * 多选时已选的项是触发按钮前面的一排 Tag(不能放进 button 里:按钮里不能有按钮),各带移除按钮。\n * 这排标签只给指针用(aria-hidden、移除按钮不进 Tab 顺序):读屏从触发按钮里念出全部已选内容,\n * 键盘在列表里取消勾选,或在触发按钮(及空的搜索框)里按 Backspace 移除最后一项(同 TagsInput)。\n * maxTagCount 是数字时只显示前几个,'responsive' 时排成一行按宽度放,其余收进“+N”(SelectedTags,TreeSelect 共用)。\n *\n * 键盘:关闭时 ↓ / Enter / 空格打开(↑ 打开并高亮最后一项),单选时直接按字母选中以它开头的项(同原生 select);\n * 打开时 ↑↓、Home / End、Page Up / Down 移动,Enter / 空格选中(单选随后关闭),Esc、Tab 关闭。\n * 写了 name 时用隐藏的 input 随表单提交(多选时每个值一个)。\n *\n * searchable 时弹出层顶部有搜索框:打开后焦点在搜索框里,输入就过滤选项(按显示文字,忽略大小写与重音,或用 filter),\n * ↑↓ 移动、Enter 选中、Esc 关闭并把焦点还给触发按钮。已选的项被过滤掉、或 items 换了(远程搜索)时,\n * 触发框里仍显示它的文字(useSelectedOptions)。\n *\n * 远程选项:给 load 时打开就按搜索词请求,输入停下 300 ms 再请求,只采用最后一次(remote-items.ts);\n * 自己管请求时写 _search={keyword} 拿到搜索词、filter={false} 关掉本地过滤,items 与 loading 自己给。\n *\n * <Select searchable load={(q, signal) => http.get<User[]>('/users', { query: { q }, signal })} toOption={…} _value={owner} />\n *\n * creatable 时自带搜索框,输入的文字没有对应的选项时列表末尾多一项“新建“…””:选中后值就是这段文字,onCreate 报告它。\n *\n * 部件:Root(外壳)、Tags(多选时的那排标签)、Tag、Trigger(button,主元素)、Value、Clear、Chevron、Popover、\n * Search(TextField)、Listbox。\n */\nimport { ChevronDown, Plus, Search } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { useId, watch, type VNodeChild } from 'vue';\nimport type { PresetIconButtonProps } from '../base/CloseButton';\nimport { buttonBaseStyles } from '../base/button-core';\nimport { Icon, type IconProps } from '../base/Icon';\nimport type { TagProps } from '../base/Tag';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { useLang, useTextMatcher } from '../config/context';\nimport { useAnchoredPopover, type FloatingPlacement } from '../utils/dom/floating';\nimport { openSetter, type OpenChangeProps } from '../utils/dom/open-change';\nimport { ListPopover } from './ListPopover';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { selectedValues, type SelectionProps } from '../utils/data/selection';\nimport {\n controlClear,\n controlParts,\n clickOnShell,\n focusOnShell,\n controlIcon,\n useControlAppearance,\n type ControlIconOptions,\n type ControlOptions,\n} from './control';\nimport { useFieldControl } from '../form/Field';\nimport { FormValue } from '../form/FormValue';\nimport { disclosureStyles } from '../utils/style/transitions';\nimport { activeOptionId, type ListboxViewProps, type OptionRenderState } from './Listbox';\nimport {\n handleListKeydown,\n useListState,\n useResultAnnouncement,\n useSelectedOptions,\n type ListboxOption,\n type OptionMapping,\n toListboxOption,\n} from './list-state';\nimport { useRemoteItems, type OptionsLoader } from './remote-items';\nimport { SelectedTags, type SelectedTagsProps } from './SelectedTags';\nimport { TextField, type TextFieldProps } from './TextField';\n\n/** 角色、展开状态与可用性由组件管理 */\nexport type SelectManaged =\n | 'type'\n | 'role'\n | 'value'\n | 'disabled'\n | 'aria-haspopup'\n | 'aria-expanded'\n | 'aria-controls'\n | 'aria-activedescendant'\n | 'aria-invalid'\n | 'aria-required'\n | 'aria-describedby';\n\n/** 下拉选择共有的属性;未声明的属性落到触发按钮(主元素)上,外壳用 propsRoot */\nexport interface SelectBaseProps<T>\n extends PartProps<'button', SelectManaged>, ControlOptions, ControlIconOptions, OpenChangeProps {\n /** 数据:ListboxOption、字符串,或任意数据加 toOption;有 load 时是第一次加载之前的选项(编辑页放上已选的那一项) */\n items?: readonly T[];\n /** 没有选择时显示的文字,默认取语言包(请选择) */\n placeholder?: string;\n /** 是否打开:父级可以写 _open={x} */\n _open?: boolean;\n /** 弹出位置,默认 bottom-start */\n placement?: FloatingPlacement;\n /** 有选择时显示清空按钮 */\n clearable?: boolean;\n /** 弹出层顶部带搜索框,输入过滤选项:选项多(供应商、人员、件号)时用 */\n searchable?: boolean;\n /** 搜索时怎样算匹配;默认显示文字包含输入(忽略大小写与重音)。false 时不在本地过滤(服务端按 search 过滤) */\n filter?: false | ((item: T, query: string) => boolean);\n /** 搜索框里的文字:打开时清空;父级写 _search={x} 拿到它(自己发请求时) */\n _search?: string;\n /** 远程选项:打开时与搜索词停下 300 ms 后调用,结果代替 items;只采用最后一次,被顶掉的请求 signal 中止 */\n load?: OptionsLoader<T>;\n /**\n * 可以新建选项(自带搜索框):输入的文字没有对应的选项时,列表末尾多一项“新建“…””,选中后值就是这段文字。\n * 在 onCreate 里把它加进 items(或存到服务端);不加时触发框里照样显示这段文字\n */\n creatable?: boolean;\n /** 新建了一项(creatable 时):参数是输入的文字,也就是选中的值 */\n onCreate?: (text: string) => void;\n /** 多选时触发框里最多显示几个标签,其余收进“+N”;'responsive' 时排成一行、按宽度放,放不下的收起 */\n maxTagCount?: number | 'responsive';\n /** 表单提交时的 name(用隐藏的 input) */\n name?: string;\n /** 不可用 */\n disabled?: boolean;\n /** 只读:能聚焦、能看到已选的内容,不能打开、不能清空 */\n readonly?: boolean;\n /** 必填 */\n required?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 自定义选项内容:`{(option, { selected, active }) => …}` */\n children?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 自定义触发框里显示的内容(已选的选项);默认单选显示文字、多选显示标签 */\n renderValue?: (selected: readonly T[]) => VNodeChild;\n /** 正在取选项(异步加载):列表里换成骨架行 */\n loading?: boolean;\n /** 没有选项时列表里显示的内容 */\n empty?: VNodeChild;\n /** 外壳的部件属性 */\n propsRoot?: PartProps<'div'>;\n /** 已选内容那一层的部件属性 */\n propsValue?: PartProps<'span'>;\n /** 多选时每个标签的部件属性 */\n propsTag?: Partial<TagProps>;\n /** 多选时那排标签的部件属性 */\n propsTags?: Partial<SelectedTagsProps>;\n /** 清空按钮的部件属性 */\n propsClear?: Partial<PresetIconButtonProps>;\n /** 下拉箭头的部件属性 */\n propsChevron?: Partial<IconProps>;\n /** 弹出层的部件属性 */\n propsPopover?: PartProps<'div', 'id' | 'popover'>;\n /** 列表的部件属性 */\n propsListbox?: Partial<ListboxViewProps<T>>;\n /** 搜索框的部件属性(searchable 时) */\n propsSearch?: Partial<TextFieldProps>;\n}\n\n/** 单选或多选;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type SelectProps<T = ListboxOption> = SelectBaseProps<T> & SelectionProps & OptionMapping<T>;\n\n/** Select 的样式:root、trigger、value、placeholder、chevronOpen 等 */\nexport const selectStyles = css.create({\n // 在 ControlGroup 里也按内容宽(输入框才分剩余宽度)\n root: { cursor: 'pointer', flexGrow: 0 },\n // 去掉按钮默认外观用 buttonBaseStyles;像输入框一样没有自己的焦点环与波纹(焦点环画在外壳上)\n trigger: {\n display: 'flex',\n alignItems: 'center',\n flexGrow: 1,\n minWidth: 0,\n alignSelf: 'stretch',\n textAlign: 'start',\n cursor: 'inherit',\n outlineStyle: 'none',\n },\n value: {\n display: 'flex',\n flexWrap: 'wrap',\n alignItems: 'center',\n gap: '_xs',\n minWidth: 0,\n paddingBlock: '_xs',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n },\n // 是真正的文字(不是原生 placeholder),要满足 4.5:1:用 textMuted 而不是 textPlaceholder\n placeholder: { color: '_textMuted' },\n // 多选时标签排在触发按钮前面(SelectedTags)占剩余宽度,触发按钮里只剩读屏用的文字\n triggerBehindTags: { flexGrow: 0 },\n chevronOpen: { rotate: '180deg' },\n // 弹出层顶部的搜索框占满宽度\n search: { width: '_full' },\n});\n\n/** creatable 时列表末尾那一项“新建“…””:不是 items 里的数据 */\nclass Creation {\n readonly text: string;\n constructor(text: string) {\n this.text = text;\n }\n}\n\n/** 下拉选择:触发框 + 弹出的选项列表 */\nexport const Select = _component(\n <T,>({\n items = [],\n toOption,\n selectionMode = 'single',\n _value: value = _bindable<string | string[] | null>(null),\n _open: open = _bindable(false),\n onOpenChange,\n placeholder,\n placement = 'bottom-start',\n clearable = false,\n searchable = false,\n filter,\n _search: search = _bindable(''),\n load,\n creatable = false,\n onCreate,\n maxTagCount,\n name,\n variant,\n size,\n radius,\n disabled,\n readonly,\n required,\n invalid,\n id,\n icon,\n children,\n renderValue,\n empty,\n loading = false,\n propsRoot,\n propsIcon,\n propsValue,\n propsTag,\n propsTags,\n propsClear,\n propsChevron,\n propsPopover,\n propsListbox,\n propsSearch,\n ...rest\n }: SelectProps<T>) => {\n const lang = useLang();\n const textMatcher = useTextMatcher();\n const listId = useId();\n const appearance = useControlAppearance(() => ({ variant, size, radius }));\n const control = useFieldControl(() => ({ id, invalid, required, disabled, readonly }));\n const multiple = () => selectionMode === 'multiple';\n const optionOf = (item: T) => toListboxOption(item, toOption);\n const remote = useRemoteItems({\n load: () => load,\n query: () => search,\n open: () => open,\n items: () => items,\n });\n // 本地过滤:有 load 或 filter={false} 时交给服务端\n const localFilter = () => !load && filter !== false;\n const matches = (item: T, text: string) =>\n filter ? filter(item, text) : textMatcher()(optionOf(item).label, text);\n // creatable 自带搜索框\n const withSearch = () => searchable || creatable;\n // creatable:输入的文字没有同名(或同值)的选项时,列表末尾多一项“新建”\n const creation = () => {\n const text = search.trim();\n if (!creatable || !text) return undefined;\n const exists = remote.items().some((item) => {\n const option = optionOf(item);\n return option.value === text || option.label.toLowerCase() === text.toLowerCase();\n });\n return exists ? undefined : new Creation(text);\n };\n const list = useListState<T | Creation>({\n items: () => {\n const all = remote.items();\n const shown =\n withSearch() && search && localFilter()\n ? all.filter((item) => matches(item, search))\n : all;\n const created = creation();\n return created ? [...shown, created] : shown;\n },\n toOption: (item) =>\n item instanceof Creation\n ? { value: item.text, label: lang.selectCreate(item.text), icon: Plus }\n : optionOf(item),\n selected: () => selectedValues(value),\n mode: () => selectionMode,\n onChange: (next) => {\n const created = creation();\n if (created && next.includes(created.text)) {\n onCreate?.(created.text);\n search = '';\n }\n value = multiple() ? [...next] : (next[0] ?? null);\n },\n // 单选时再点已选的一项不取消(同原生 select);要清空用清除按钮\n required: () => !multiple(),\n });\n // 已选项的文字取自全部 items(过滤掉的、远程搜索换掉的也在)\n const selectedOptions = useSelectedOptions({ items: remote.items, toOption: optionOf });\n let shell = _state.raw<HTMLElement | null>(null);\n let trigger = _state.raw<HTMLElement | null>(null);\n let panel = _state.raw<HTMLElement | null>(null);\n const setOpen = openSetter(\n () => open,\n (next) => (open = next),\n () => onOpenChange,\n );\n\n /** 多选时移除一项(标签的移除按钮、Backspace) */\n function remove(target: string) {\n value = selectedValues(value).filter((item) => item !== target);\n }\n\n function removeLast() {\n const last = selectedValues(value).at(-1);\n if (last !== undefined) remove(last);\n }\n\n function show(highlight: 'default' | 'last' = 'default', event?: Event) {\n search = '';\n list.activateDefault();\n if (highlight === 'last' && selectedValues(value).length === 0) list.move('last');\n setOpen(true, 'trigger', event);\n }\n\n useResultAnnouncement(() => ({ query: search, count: list.options.length }));\n\n // 输入搜索词:高亮第一个匹配的。打开时清空搜索词不算:那时高亮的是已选项\n watch(\n () => search,\n (text) => text && list.move('first'),\n );\n\n // 选中一项:单选时关闭\n function picked(event?: Event) {\n if (!multiple()) setOpen(false, 'select', event);\n }\n\n /** Ctrl / Cmd / Alt 组合键是浏览器的(刷新、复制、查找),不做按字母跳转 */\n const hasModifier = (event: KeyboardEvent) => event.altKey || event.ctrlKey || event.metaKey;\n\n function onKeydown(event: KeyboardEvent) {\n if (control().disabled || control().readonly) return;\n if (event.key === 'Backspace' && multiple()) {\n event.preventDefault();\n removeLast();\n return;\n }\n if (!open) {\n if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n show('default', event);\n } else if (event.key === 'ArrowUp') {\n event.preventDefault();\n show('last', event);\n } else if (!multiple() && !hasModifier(event) && list.typeahead(event.key)) {\n // 同原生 select:关闭时按字母直接选中\n event.preventDefault();\n if (list.active !== undefined && !list.isSelected(list.active)) list.select(list.active);\n }\n return;\n }\n if (event.key === 'Tab') {\n setOpen(false, 'focus', event);\n return;\n }\n if (handleListKeydown(list, event, { space: true, typeahead: true }, () => picked(event))) {\n event.preventDefault();\n }\n }\n\n // 搜索框里:↑↓ 移动、Enter 选中(Home / End、空格留给输入),Tab 关闭后照常移到下一个;\n // 多选时空的搜索框里按 Backspace 移除最后一项\n function onSearchKeydown(event: KeyboardEvent) {\n if (event.key === 'Tab') {\n setOpen(false, 'focus', event);\n return;\n }\n if (event.key === 'Backspace' && !search && multiple()) {\n event.preventDefault();\n removeLast();\n return;\n }\n if (handleListKeydown(list, event, { homeEnd: false }, () => picked(event)))\n event.preventDefault();\n }\n\n function clear() {\n value = multiple() ? [] : null;\n trigger?.focus();\n }\n\n // 弹层贴着外壳、与它同宽;焦点一直在触发按钮上,关闭时不用归还(searchable 时见 onPanelToggle)\n const { onToggle } = useAnchoredPopover(\n () => ({\n open,\n anchor: shell,\n panel,\n floating: { placement, matchWidth: true, fitHeight: true },\n restoreFocus: false,\n }),\n { close: (reason, event) => setOpen(false, reason, event) },\n );\n\n // searchable:打开后焦点进搜索框;关闭时焦点还在弹层里(Esc、选中、点外面)就还给触发按钮\n function onPanelToggle(event: ToggleEvent) {\n onToggle(event);\n if (!withSearch()) return;\n if (event.newState === 'open') {\n panel?.querySelector<HTMLInputElement>('input[type=\"search\"]')?.focus();\n return;\n }\n const active = document.activeElement;\n if (!active || active === document.body || panel?.contains(active)) trigger?.focus();\n }\n\n return () => {\n const state = control();\n const values = selectedValues(value);\n const selected = selectedOptions(values);\n const off = state.disabled;\n // 只读:能聚焦,不能打开、不能改\n const locked = off || state.readonly;\n // 多选的默认显示:触发按钮前面一排标签,按钮里只留给读屏的文字\n const tagged = multiple() && !renderValue && selected.length > 0;\n const shown = tagged ? (\n <span {...css.attrs(visuallyHiddenStyles.hidden)}>\n {lang.selectSummary(selected.map((option) => option.label))}\n </span>\n ) : (\n (renderValue?.(\n selected.flatMap((option) => (option.item === undefined ? [] : [option.item])),\n ) ??\n (selected.length === 0 ? (\n <span {...css.attrs(selectStyles.placeholder)}>\n {placeholder ?? lang.selectPlaceholder}\n </span>\n ) : (\n selected[0]!.label\n )))\n );\n const tags = tagged && (\n <SelectedTags\n {...mergePartProps<SelectedTagsProps>(\n {\n tags: selected,\n maxTagCount,\n onRemove: locked ? undefined : remove,\n propsTag,\n },\n propsTags,\n )}\n />\n );\n const s = controlParts(appearance(), state);\n return (\n <>\n <div\n {...mergeAttrs<'div'>({ ...s.root, sx: [s.root.sx, selectStyles.root] }, propsRoot, {\n ref: _toRef(shell),\n // 点在外壳空白处(含多选的标签):焦点交给触发按钮,并与点触发按钮一样切换打开(只读时只聚焦)\n onPointerdown: off ? undefined : focusOnShell(() => trigger),\n onClick: locked\n ? undefined\n : clickOnShell(\n () => trigger,\n (event) => (open ? setOpen(false, 'trigger', event) : show('default', event)),\n ),\n })}\n data-state={open ? 'open' : 'closed'}\n data-invalid={state.invalid || undefined}\n data-disabled={off || undefined}\n >\n {controlIcon(s, icon, propsIcon)}\n {tags}\n <button\n {...mergeAttrs<'button'>(\n {\n sx: [\n buttonBaseStyles.root,\n selectStyles.trigger,\n tagged && selectStyles.triggerBehindTags,\n ],\n },\n rest,\n {\n ref: _toRef(trigger),\n id: state.id,\n type: 'button',\n role: 'combobox',\n disabled: off,\n 'aria-haspopup': 'listbox',\n 'aria-expanded': open,\n 'aria-controls': listId,\n 'aria-activedescendant': open ? activeOptionId(list, listId) : undefined,\n 'aria-invalid': state.invalid || undefined,\n 'aria-required': state.required || undefined,\n 'aria-readonly': state.readonly || undefined,\n 'aria-describedby': state.describedBy,\n onClick: (event: MouseEvent) => {\n // Safari 点击按钮不会聚焦它:焦点要在触发按钮上,键盘与 aria-activedescendant 才有效\n trigger?.focus();\n if (locked) return;\n if (open) setOpen(false, 'trigger', event);\n else show('default', event);\n },\n onKeydown,\n },\n )}\n >\n <span {...mergeAttrs<'span'>({ sx: selectStyles.value }, propsValue)}>{shown}</span>\n </button>\n {clearable && values.length > 0 && !locked && controlClear(s, propsClear, clear)}\n <Icon\n {...mergePartProps<IconProps>(\n {\n ...s.icon,\n icon: ChevronDown,\n sx: [s.icon.sx, disclosureStyles.icon, open && selectStyles.chevronOpen],\n },\n propsChevron,\n )}\n />\n <FormValue name={name} value={values} disabled={state.disabled} />\n </div>\n <ListPopover\n panelRef={_toRef(panel)}\n onToggle={onPanelToggle}\n size={appearance().size}\n labelledBy={state.labelledBy}\n label={rest['aria-label']}\n loading={loading || remote.loading()}\n onPick={() => picked()}\n header={\n withSearch() && (\n <TextField\n {...mergePartProps<TextFieldProps>(\n {\n icon: Search,\n variant: 'soft',\n placeholder: lang.selectSearch,\n 'aria-label': lang.selectSearch,\n propsRoot: { sx: selectStyles.search },\n },\n propsSearch,\n )}\n type=\"search\"\n _value={search}\n aria-controls={listId}\n aria-activedescendant={open ? activeOptionId(list, listId) : undefined}\n onKeydown={onSearchKeydown}\n />\n )\n }\n listbox={{\n list,\n id: listId,\n empty: remote.failure() ?? (search ? lang.selectNoMatch : empty),\n renderOption:\n children &&\n ((item: T | Creation, state: OptionRenderState) =>\n item instanceof Creation ? lang.selectCreate(item.text) : children(item, state)),\n multiple: multiple(),\n }}\n // 使用方按 T 写列表的部件属性;creatable 时多出的“新建”那一项由上面的 renderOption 处理\n propsListbox={propsListbox as Partial<ListboxViewProps<T | Creation>> | undefined}\n propsPopover={propsPopover}\n />\n </>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@ export interface CommandPaletteBaseProps<T> extends Omit<DialogProps, 'open' | '
|
|
|
26
26
|
_inputValue?: string;
|
|
27
27
|
/** 过滤:默认按选项文字与说明匹配(忽略大小写与重音);函数自定义;false 不过滤(远程搜索,items 就是结果) */
|
|
28
28
|
filter?: false | ((item: T, query: string) => boolean);
|
|
29
|
-
/**
|
|
29
|
+
/** 正在取结果(远程搜索):列表里换成骨架行 */
|
|
30
30
|
loading?: boolean;
|
|
31
31
|
/** 打开面板的快捷键,默认 'Mod+K';false 不注册 */
|
|
32
32
|
hotkey?: string | false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommandPalette.js","names":["_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_createVNode","_Fragment","_mergeProps","_withDirectives","_vModelText"],"sources":["../../src/overlay/CommandPalette.tsx"],"sourcesContent":["/**\n * 命令面板:Mod+K(macOS 上 ⌘K,其他系统 Ctrl+K)或点触发按钮打开的搜索对话框,边输入边过滤,回车打开。\n * 文档站的站内搜索、后台的“跳转到…”都用它;数据由使用方给,选中后做什么也由使用方决定。\n *\n * <CommandPalette\n * items={pages}\n * toOption={(page) => ({ value: page.path, label: page.title, description: page.summary, group: page.section })}\n * onAction={(page) => router.push(page.path)}\n * />\n *\n * 结构是 Dialog(模态、焦点循环、Esc 关闭)里放一个 combobox 输入框与 ListboxView:\n * 焦点一直在输入框上,↑↓ / Page Up / Page Down 移动高亮(aria-activedescendant),Enter 选中;\n * 数据层与 Select、Combobox 相同(useListState),选项多时虚拟滚动。\n * 远程搜索与 Combobox 一样:_inputValue={keyword} 拿到输入,filter={false} 不再本地过滤,取数据时给 loading。\n */\nimport { Search } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { useId, watch, type VNodeChild } from 'vue';\nimport { ButtonBase, type ButtonBaseProps } from '../base/ButtonBase';\nimport { Icon } from '../base/Icon';\nimport { Kbd } from '../base/Kbd';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { useLang, useTextMatcher } from '../config/context';\nimport { controlParts, useControlAppearance, type ControlParts } from '../input/control';\nimport {\n ListboxView,\n activeOptionId,\n type ListboxViewProps,\n type OptionRenderState,\n} from '../input/Listbox';\nimport {\n handleListKeydown,\n useListState,\n type ListboxOption,\n type OptionMapping,\n toListboxOption,\n} from '../input/list-state';\nimport { ariaKeyShortcut, formatHotkey, useHotkeys } from '../utils/dom/hotkeys';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { breakpoints } from '../config/theme/tokens';\nimport { openSetter } from '../utils/dom/open-change';\nimport { Dialog, type DialogProps } from './Dialog';\n\n/** 交给触发器的属性:展开到按钮上 */\nexport interface CommandPaletteTriggerProps {\n /** 打开面板 */\n onClick: (event: MouseEvent) => void;\n /** 告诉读屏这个按钮会打开对话框 */\n 'aria-haspopup': 'dialog';\n /** 面板是否打开 */\n 'aria-expanded': boolean;\n}\n\n/** 与数据类型无关的属性;未声明的属性落到对话框(dialog)上 */\nexport interface CommandPaletteBaseProps<T> extends Omit<\n DialogProps,\n 'open' | 'title' | 'description' | 'footer' | 'children' | 'onSelect'\n> {\n /** 是否打开:父级可以写 _open={x};快捷键、触发按钮、选中、Esc 都会改它 */\n _open?: boolean;\n /** 数据:ListboxOption、字符串,或任意数据加 toOption */\n items: readonly T[];\n /** 执行了一项(回车或点击):面板随即关闭 */\n onAction: (item: T) => void;\n /** 输入框里的文字:父级可以写 _inputValue={x}(远程搜索时据此取数据);每次打开清空 */\n _inputValue?: string;\n /** 过滤:默认按选项文字与说明匹配(忽略大小写与重音);函数自定义;false 不过滤(远程搜索,items 就是结果) */\n filter?: false | ((item: T, query: string) => boolean);\n /** 正在取结果(远程搜索):没有结果时列表里是骨架行,已有结果时顶部显示进度条 */\n loading?: boolean;\n /** 打开面板的快捷键,默认 'Mod+K';false 不注册 */\n hotkey?: string | false;\n /** 输入框的占位文字,默认取语言包(搜索文档) */\n placeholder?: string;\n /** 面板的名称(读屏念的),默认取语言包(搜索) */\n label?: string;\n /** 触发器:`(props) => <IconButton {...props} icon={Search} aria-label=\"搜索\" />`;默认是一个像搜索框的按钮,false 不渲染 */\n trigger?: ((props: CommandPaletteTriggerProps) => VNodeChild) | false;\n /** 自定义选项内容 */\n renderOption?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 没有匹配时显示的内容;默认是 Empty(文案取语言包) */\n empty?: VNodeChild;\n /** 默认触发器的部件属性 */\n propsTrigger?: Partial<ButtonBaseProps>;\n /** 输入框的部件属性 */\n propsInput?: PartProps<'input'>;\n /** 列表的部件属性 */\n propsListbox?: Partial<ListboxViewProps<T>>;\n}\n\n/** 命令面板的属性;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type CommandPaletteProps<T = ListboxOption> = CommandPaletteBaseProps<T> & OptionMapping<T>;\n\n/** CommandPalette 的样式:dialog、search、input、listbox、trigger、triggerText、triggerLabel */\nexport const commandPaletteStyles = css.create({\n // 停在视口上部而不是正中:结果变多变少时输入框不跳\n dialog: { marginBlockStart: '12dvh' },\n search: {\n display: 'flex',\n alignItems: 'center',\n gap: '_sm',\n paddingBlock: '_sm',\n paddingInline: '_md',\n borderBlockEndWidth: '_xs',\n borderBlockEndStyle: 'solid',\n borderBlockEndColor: '_border',\n fontSize: '_lg',\n },\n input: { paddingBlock: '_sm' },\n listbox: { maxHeight: 'min(24rem, 50dvh)' },\n body: { padding: 0 },\n // 窄屏(40rem 以下,sizes.sm)只留放大镜图标,不挤占顶栏\n trigger: {\n gap: '_sm',\n minWidth: { default: null, [breakpoints.sm]: '12rem' },\n color: '_textMuted',\n cursor: 'pointer',\n },\n triggerText: { display: { default: 'none', [breakpoints.sm]: 'initial' } },\n // 按钮的名称来自这段文字(与看得见的一致,WCAG 2.5.3):窄屏只留图标时视觉上隐藏,读屏仍念得到\n triggerLabel: {\n flexGrow: 1,\n textAlign: 'start',\n whiteSpace: 'nowrap',\n position: { default: 'absolute', [breakpoints.sm]: 'static' },\n width: { default: 1, [breakpoints.sm]: 'auto' },\n height: { default: 1, [breakpoints.sm]: 'auto' },\n overflow: { default: 'hidden', [breakpoints.sm]: 'visible' },\n clipPath: { default: 'inset(50%)', [breakpoints.sm]: 'none' },\n },\n});\n\n/** 命令面板:快捷键打开的搜索对话框 */\nexport const CommandPalette = _component(\n <T,>({\n _open: open = _bindable(false),\n onOpenChange,\n _inputValue: inputValue = _bindable(''),\n items,\n toOption,\n onAction,\n filter,\n loading = false,\n hotkey = 'Mod+K',\n placeholder,\n label,\n trigger,\n renderOption,\n empty,\n propsTrigger,\n propsInput,\n propsListbox,\n ...rest\n }: CommandPaletteProps<T>) => {\n const lang = useLang();\n const textMatcher = useTextMatcher();\n const listId = useId();\n const appearance = useControlAppearance(() => ({}));\n const optionOf = (item: T) => toListboxOption(item, toOption);\n const setOpen = openSetter(\n () => open,\n (next) => (open = next),\n () => onOpenChange,\n );\n\n function matches(item: T, text: string) {\n if (filter === false) return true;\n if (filter) return filter(item, text);\n const match = textMatcher();\n const option = optionOf(item);\n return match(option.label, text) || match(option.description ?? '', text);\n }\n\n const list = useListState({\n items: () => (inputValue ? items.filter((item) => matches(item, inputValue)) : items),\n toOption: optionOf,\n // 面板不记选择:选中就是执行\n selected: () => [],\n mode: () => 'single',\n onChange: () => {},\n });\n\n // 每次打开从空白开始;输入变了高亮第一个结果\n watch(\n () => open,\n (isOpen) => {\n if (isOpen) inputValue = '';\n },\n );\n // 远程搜索时结果晚到:结果变了也高亮第一个\n watch([() => inputValue, () => open, () => list.options.length], () => list.move('first'), {\n flush: 'post',\n });\n\n useHotkeys(() => (hotkey ? { [hotkey]: (event) => setOpen(!open, 'imperative', event) } : {}), {\n inInputs: true,\n });\n\n function pick(value: string, event?: Event) {\n const entry = list.options[list.indexOf(value)];\n if (!entry || entry.disabled) return;\n setOpen(false, 'select', event);\n onAction(entry.item);\n }\n\n function onKeydown(event: KeyboardEvent) {\n if (handleListKeydown(list, event, { homeEnd: false }, (value) => pick(value, event)))\n event.preventDefault();\n }\n\n const triggerProps = (): CommandPaletteTriggerProps => ({\n onClick: (event: MouseEvent) => setOpen(true, 'trigger', event),\n 'aria-haspopup': 'dialog',\n 'aria-expanded': open,\n });\n\n /** 默认的触发按钮:画成输入框外壳(只取样式,部件标记归面板自己) */\n function renderTrigger(shell: ControlParts) {\n if (trigger === false) return null;\n if (trigger) return trigger(triggerProps());\n const text = placeholder ?? lang.searchPlaceholder;\n return (\n <ButtonBase\n {...mergePartProps<ButtonBaseProps>(\n {\n sx: [shell.root.sx, commandPaletteStyles.trigger],\n // 快捷键由 aria-keyshortcuts 告诉读屏,显示它的 Kbd 不算进名称\n 'aria-keyshortcuts': hotkey ? ariaKeyShortcut(hotkey) : undefined,\n },\n propsTrigger,\n triggerProps(),\n )}\n >\n <Icon icon={Search} sx={shell.icon.sx} />\n <span {...mergeAttrs<'span'>({ sx: commandPaletteStyles.triggerLabel })}>{text}</span>\n {hotkey && (\n <Kbd\n sx={commandPaletteStyles.triggerText}\n aria-hidden={ariaKeyShortcut(hotkey) ? 'true' : undefined}\n >\n {formatHotkey(hotkey)}\n </Kbd>\n )}\n </ButtonBase>\n );\n }\n\n return () => {\n const shell = controlParts(appearance(), {});\n return (\n <>\n {renderTrigger(shell)}\n <Dialog\n {...mergePartProps<DialogProps>(\n {\n title: label ?? lang.searchLabel,\n sx: commandPaletteStyles.dialog,\n size: 'sm',\n closable: false,\n // 标题只给读屏:面板的名称\n propsHeader: { sx: visuallyHiddenStyles.hidden },\n propsBody: { propsContent: { sx: commandPaletteStyles.body } },\n },\n rest as Partial<DialogProps>,\n )}\n _open={open}\n onOpenChange={onOpenChange}\n >\n <div {...mergeAttrs<'div'>({ sx: commandPaletteStyles.search })}>\n <Icon icon={Search} sx={shell.icon.sx} />\n {/* _bind 编译为 v-model:输入法组合期间不过滤 */}\n <input\n {...mergeAttrs<'input'>(\n { sx: [shell.input.sx, commandPaletteStyles.input] },\n propsInput,\n {\n role: 'combobox',\n type: 'search',\n autofocus: true,\n autocomplete: 'off',\n enterkeyhint: 'go',\n placeholder: placeholder ?? lang.searchPlaceholder,\n 'aria-label': label ?? lang.searchLabel,\n 'aria-autocomplete': 'list',\n 'aria-expanded': true,\n 'aria-controls': listId,\n 'aria-activedescendant': activeOptionId(list, listId),\n onKeydown,\n },\n )}\n _value={inputValue}\n />\n </div>\n <ListboxView\n {...mergePartProps<ListboxViewProps<T>>(\n {\n sx: commandPaletteStyles.listbox,\n list,\n id: listId,\n renderOption,\n empty,\n loading,\n propsEmpty: { description: lang.searchEmpty },\n },\n propsListbox,\n { id: listId, 'aria-label': label ?? lang.searchLabel, onPick: pick },\n )}\n />\n </Dialog>\n </>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CommandPalette.js","names":["_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_createVNode","_Fragment","_mergeProps","_withDirectives","_vModelText"],"sources":["../../src/overlay/CommandPalette.tsx"],"sourcesContent":["/**\n * 命令面板:Mod+K(macOS 上 ⌘K,其他系统 Ctrl+K)或点触发按钮打开的搜索对话框,边输入边过滤,回车打开。\n * 文档站的站内搜索、后台的“跳转到…”都用它;数据由使用方给,选中后做什么也由使用方决定。\n *\n * <CommandPalette\n * items={pages}\n * toOption={(page) => ({ value: page.path, label: page.title, description: page.summary, group: page.section })}\n * onAction={(page) => router.push(page.path)}\n * />\n *\n * 结构是 Dialog(模态、焦点循环、Esc 关闭)里放一个 combobox 输入框与 ListboxView:\n * 焦点一直在输入框上,↑↓ / Page Up / Page Down 移动高亮(aria-activedescendant),Enter 选中;\n * 数据层与 Select、Combobox 相同(useListState),选项多时虚拟滚动。\n * 远程搜索与 Combobox 一样:_inputValue={keyword} 拿到输入,filter={false} 不再本地过滤,取数据时给 loading。\n */\nimport { Search } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { useId, watch, type VNodeChild } from 'vue';\nimport { ButtonBase, type ButtonBaseProps } from '../base/ButtonBase';\nimport { Icon } from '../base/Icon';\nimport { Kbd } from '../base/Kbd';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { useLang, useTextMatcher } from '../config/context';\nimport { controlParts, useControlAppearance, type ControlParts } from '../input/control';\nimport {\n ListboxView,\n activeOptionId,\n type ListboxViewProps,\n type OptionRenderState,\n} from '../input/Listbox';\nimport {\n handleListKeydown,\n useListState,\n type ListboxOption,\n type OptionMapping,\n toListboxOption,\n} from '../input/list-state';\nimport { ariaKeyShortcut, formatHotkey, useHotkeys } from '../utils/dom/hotkeys';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { breakpoints } from '../config/theme/tokens';\nimport { openSetter } from '../utils/dom/open-change';\nimport { Dialog, type DialogProps } from './Dialog';\n\n/** 交给触发器的属性:展开到按钮上 */\nexport interface CommandPaletteTriggerProps {\n /** 打开面板 */\n onClick: (event: MouseEvent) => void;\n /** 告诉读屏这个按钮会打开对话框 */\n 'aria-haspopup': 'dialog';\n /** 面板是否打开 */\n 'aria-expanded': boolean;\n}\n\n/** 与数据类型无关的属性;未声明的属性落到对话框(dialog)上 */\nexport interface CommandPaletteBaseProps<T> extends Omit<\n DialogProps,\n 'open' | 'title' | 'description' | 'footer' | 'children' | 'onSelect'\n> {\n /** 是否打开:父级可以写 _open={x};快捷键、触发按钮、选中、Esc 都会改它 */\n _open?: boolean;\n /** 数据:ListboxOption、字符串,或任意数据加 toOption */\n items: readonly T[];\n /** 执行了一项(回车或点击):面板随即关闭 */\n onAction: (item: T) => void;\n /** 输入框里的文字:父级可以写 _inputValue={x}(远程搜索时据此取数据);每次打开清空 */\n _inputValue?: string;\n /** 过滤:默认按选项文字与说明匹配(忽略大小写与重音);函数自定义;false 不过滤(远程搜索,items 就是结果) */\n filter?: false | ((item: T, query: string) => boolean);\n /** 正在取结果(远程搜索):列表里换成骨架行 */\n loading?: boolean;\n /** 打开面板的快捷键,默认 'Mod+K';false 不注册 */\n hotkey?: string | false;\n /** 输入框的占位文字,默认取语言包(搜索文档) */\n placeholder?: string;\n /** 面板的名称(读屏念的),默认取语言包(搜索) */\n label?: string;\n /** 触发器:`(props) => <IconButton {...props} icon={Search} aria-label=\"搜索\" />`;默认是一个像搜索框的按钮,false 不渲染 */\n trigger?: ((props: CommandPaletteTriggerProps) => VNodeChild) | false;\n /** 自定义选项内容 */\n renderOption?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 没有匹配时显示的内容;默认是 Empty(文案取语言包) */\n empty?: VNodeChild;\n /** 默认触发器的部件属性 */\n propsTrigger?: Partial<ButtonBaseProps>;\n /** 输入框的部件属性 */\n propsInput?: PartProps<'input'>;\n /** 列表的部件属性 */\n propsListbox?: Partial<ListboxViewProps<T>>;\n}\n\n/** 命令面板的属性;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type CommandPaletteProps<T = ListboxOption> = CommandPaletteBaseProps<T> & OptionMapping<T>;\n\n/** CommandPalette 的样式:dialog、search、input、listbox、trigger、triggerText、triggerLabel */\nexport const commandPaletteStyles = css.create({\n // 停在视口上部而不是正中:结果变多变少时输入框不跳\n dialog: { marginBlockStart: '12dvh' },\n search: {\n display: 'flex',\n alignItems: 'center',\n gap: '_sm',\n paddingBlock: '_sm',\n paddingInline: '_md',\n borderBlockEndWidth: '_xs',\n borderBlockEndStyle: 'solid',\n borderBlockEndColor: '_border',\n fontSize: '_lg',\n },\n input: { paddingBlock: '_sm' },\n listbox: { maxHeight: 'min(24rem, 50dvh)' },\n body: { padding: 0 },\n // 窄屏(40rem 以下,sizes.sm)只留放大镜图标,不挤占顶栏\n trigger: {\n gap: '_sm',\n minWidth: { default: null, [breakpoints.sm]: '12rem' },\n color: '_textMuted',\n cursor: 'pointer',\n },\n triggerText: { display: { default: 'none', [breakpoints.sm]: 'initial' } },\n // 按钮的名称来自这段文字(与看得见的一致,WCAG 2.5.3):窄屏只留图标时视觉上隐藏,读屏仍念得到\n triggerLabel: {\n flexGrow: 1,\n textAlign: 'start',\n whiteSpace: 'nowrap',\n position: { default: 'absolute', [breakpoints.sm]: 'static' },\n width: { default: 1, [breakpoints.sm]: 'auto' },\n height: { default: 1, [breakpoints.sm]: 'auto' },\n overflow: { default: 'hidden', [breakpoints.sm]: 'visible' },\n clipPath: { default: 'inset(50%)', [breakpoints.sm]: 'none' },\n },\n});\n\n/** 命令面板:快捷键打开的搜索对话框 */\nexport const CommandPalette = _component(\n <T,>({\n _open: open = _bindable(false),\n onOpenChange,\n _inputValue: inputValue = _bindable(''),\n items,\n toOption,\n onAction,\n filter,\n loading = false,\n hotkey = 'Mod+K',\n placeholder,\n label,\n trigger,\n renderOption,\n empty,\n propsTrigger,\n propsInput,\n propsListbox,\n ...rest\n }: CommandPaletteProps<T>) => {\n const lang = useLang();\n const textMatcher = useTextMatcher();\n const listId = useId();\n const appearance = useControlAppearance(() => ({}));\n const optionOf = (item: T) => toListboxOption(item, toOption);\n const setOpen = openSetter(\n () => open,\n (next) => (open = next),\n () => onOpenChange,\n );\n\n function matches(item: T, text: string) {\n if (filter === false) return true;\n if (filter) return filter(item, text);\n const match = textMatcher();\n const option = optionOf(item);\n return match(option.label, text) || match(option.description ?? '', text);\n }\n\n const list = useListState({\n items: () => (inputValue ? items.filter((item) => matches(item, inputValue)) : items),\n toOption: optionOf,\n // 面板不记选择:选中就是执行\n selected: () => [],\n mode: () => 'single',\n onChange: () => {},\n });\n\n // 每次打开从空白开始;输入变了高亮第一个结果\n watch(\n () => open,\n (isOpen) => {\n if (isOpen) inputValue = '';\n },\n );\n // 远程搜索时结果晚到:结果变了也高亮第一个\n watch([() => inputValue, () => open, () => list.options.length], () => list.move('first'), {\n flush: 'post',\n });\n\n useHotkeys(() => (hotkey ? { [hotkey]: (event) => setOpen(!open, 'imperative', event) } : {}), {\n inInputs: true,\n });\n\n function pick(value: string, event?: Event) {\n const entry = list.options[list.indexOf(value)];\n if (!entry || entry.disabled) return;\n setOpen(false, 'select', event);\n onAction(entry.item);\n }\n\n function onKeydown(event: KeyboardEvent) {\n if (handleListKeydown(list, event, { homeEnd: false }, (value) => pick(value, event)))\n event.preventDefault();\n }\n\n const triggerProps = (): CommandPaletteTriggerProps => ({\n onClick: (event: MouseEvent) => setOpen(true, 'trigger', event),\n 'aria-haspopup': 'dialog',\n 'aria-expanded': open,\n });\n\n /** 默认的触发按钮:画成输入框外壳(只取样式,部件标记归面板自己) */\n function renderTrigger(shell: ControlParts) {\n if (trigger === false) return null;\n if (trigger) return trigger(triggerProps());\n const text = placeholder ?? lang.searchPlaceholder;\n return (\n <ButtonBase\n {...mergePartProps<ButtonBaseProps>(\n {\n sx: [shell.root.sx, commandPaletteStyles.trigger],\n // 快捷键由 aria-keyshortcuts 告诉读屏,显示它的 Kbd 不算进名称\n 'aria-keyshortcuts': hotkey ? ariaKeyShortcut(hotkey) : undefined,\n },\n propsTrigger,\n triggerProps(),\n )}\n >\n <Icon icon={Search} sx={shell.icon.sx} />\n <span {...mergeAttrs<'span'>({ sx: commandPaletteStyles.triggerLabel })}>{text}</span>\n {hotkey && (\n <Kbd\n sx={commandPaletteStyles.triggerText}\n aria-hidden={ariaKeyShortcut(hotkey) ? 'true' : undefined}\n >\n {formatHotkey(hotkey)}\n </Kbd>\n )}\n </ButtonBase>\n );\n }\n\n return () => {\n const shell = controlParts(appearance(), {});\n return (\n <>\n {renderTrigger(shell)}\n <Dialog\n {...mergePartProps<DialogProps>(\n {\n title: label ?? lang.searchLabel,\n sx: commandPaletteStyles.dialog,\n size: 'sm',\n closable: false,\n // 标题只给读屏:面板的名称\n propsHeader: { sx: visuallyHiddenStyles.hidden },\n propsBody: { propsContent: { sx: commandPaletteStyles.body } },\n },\n rest as Partial<DialogProps>,\n )}\n _open={open}\n onOpenChange={onOpenChange}\n >\n <div {...mergeAttrs<'div'>({ sx: commandPaletteStyles.search })}>\n <Icon icon={Search} sx={shell.icon.sx} />\n {/* _bind 编译为 v-model:输入法组合期间不过滤 */}\n <input\n {...mergeAttrs<'input'>(\n { sx: [shell.input.sx, commandPaletteStyles.input] },\n propsInput,\n {\n role: 'combobox',\n type: 'search',\n autofocus: true,\n autocomplete: 'off',\n enterkeyhint: 'go',\n placeholder: placeholder ?? lang.searchPlaceholder,\n 'aria-label': label ?? lang.searchLabel,\n 'aria-autocomplete': 'list',\n 'aria-expanded': true,\n 'aria-controls': listId,\n 'aria-activedescendant': activeOptionId(list, listId),\n onKeydown,\n },\n )}\n _value={inputValue}\n />\n </div>\n <ListboxView\n {...mergePartProps<ListboxViewProps<T>>(\n {\n sx: commandPaletteStyles.listbox,\n list,\n id: listId,\n renderOption,\n empty,\n loading,\n propsEmpty: { description: lang.searchEmpty },\n },\n propsListbox,\n { id: listId, 'aria-label': label ?? lang.searchLabel, onPick: pick },\n )}\n />\n </Dialog>\n </>\n );\n };\n },\n);\n"],"mappings":"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"}
|
package/dist/overlay/detached.js
CHANGED
|
@@ -5,10 +5,23 @@ import { createVNode, getCurrentInstance, getCurrentScope, h, onScopeDispose, re
|
|
|
5
5
|
import { _component, readonlyProp } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
6
6
|
//#region src/overlay/detached.tsx
|
|
7
7
|
/** 交给对话框内容的控制:开着没有、关上了 */
|
|
8
|
-
/**
|
|
8
|
+
/** 组件树之外的根数到第几个了:给每个根一个自己的 useId 命名段 */
|
|
9
|
+
var detachedRoots = 0;
|
|
10
|
+
/**
|
|
11
|
+
* 组件树之外的根:提供调用处的配置,内容包在 ConfigScope 里(主题变量、语言)。
|
|
12
|
+
* render() 出来的根,useId 从头计数、前缀又和应用相同(共用 appContext),生成的 id 会和页面上已有的撞上
|
|
13
|
+
* (表单对话框的 label 关联到页面里另一个控件)。照 Vue 处理异步边界的做法(markAsyncBoundary)换一个命名段:
|
|
14
|
+
* 子组件沿用父组件的 ids,所以在根上换一次就够了。
|
|
15
|
+
*/
|
|
9
16
|
var DetachedRoot = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
10
17
|
const config = readonlyProp(_props, "config");
|
|
11
18
|
const content = readonlyProp(_props, "content");
|
|
19
|
+
const instance = getCurrentInstance();
|
|
20
|
+
if (instance?.ids) instance.ids = [
|
|
21
|
+
`d${++detachedRoots}-`,
|
|
22
|
+
0,
|
|
23
|
+
0
|
|
24
|
+
];
|
|
12
25
|
ConfigContext.provide(config.value);
|
|
13
26
|
return () => createVNode(ConfigScope, { "key": "__site0" }, {
|
|
14
27
|
default: () => [content.value()],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detached.js","names":["_vtp_readonlyProp","_createVNode"],"sources":["../../src/overlay/detached.tsx"],"sourcesContent":["/**\n * 渲染到组件树之外:沿用调用处的配置(主题、语言、默认值,经 ConfigScope)与应用上下文(全局组件),\n * 生命周期跟着调用的组件——它卸载时一起收掉,不留孤儿。\n *\n * const renderDetached = useDetachedRender(); // setup 里;第三方库管的元素(图表的提示框)\n * renderDetached(<Tip />, element); // 再调一次换内容,传 null 卸载;组件卸载时自动卸载\n *\n * const mount = useDetachedDialog(); // setup 里;命令式的对话框(useConfirm、useFormDialog)\n * mount((host) => <Dialog _open={host.open} onClose={host.closed}>…</Dialog>);\n *\n * 对话框渲染到 body 末尾,关上后等离场过渡播完再卸载;调用的组件卸载时(例如切换了路由)把开着的对话框关上,\n * 照常走关闭的流程,等着结果的 `await confirm()` 也拿到结果。\n * useDetachedOverlay 在 useDetachedDialog 上面加一层句柄(close、update、closed),useDialog、useDrawer 用它。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport {\n getCurrentInstance,\n getCurrentScope,\n h,\n onScopeDispose,\n render,\n shallowRef,\n type AppContext,\n type VNodeChild,\n} from 'vue';\nimport { ConfigContext, useConfig, type UiConfig } from '../config/context';\nimport { ConfigScope } from '../config/Provider';\nimport type { OpenChangeProps, OpenChangeReasons } from '../utils/dom/open-change';\nimport { afterTransitions } from '../utils/style/transitions';\n\n/** 交给对话框内容的控制:开着没有、关上了 */\nexport interface DetachedDialogHost {\n /** 对话框开着:对话框写 _open={host.open},改成 false 就关上 */\n open: boolean;\n /** 对话框关上了(原生 close 事件里调用):等离场过渡播完卸载 */\n closed: () => void;\n}\n\n/** 组件树之外的根:提供调用处的配置,内容包在 ConfigScope
|
|
1
|
+
{"version":3,"file":"detached.js","names":["_vtp_readonlyProp","_createVNode"],"sources":["../../src/overlay/detached.tsx"],"sourcesContent":["/**\n * 渲染到组件树之外:沿用调用处的配置(主题、语言、默认值,经 ConfigScope)与应用上下文(全局组件),\n * 生命周期跟着调用的组件——它卸载时一起收掉,不留孤儿。\n *\n * const renderDetached = useDetachedRender(); // setup 里;第三方库管的元素(图表的提示框)\n * renderDetached(<Tip />, element); // 再调一次换内容,传 null 卸载;组件卸载时自动卸载\n *\n * const mount = useDetachedDialog(); // setup 里;命令式的对话框(useConfirm、useFormDialog)\n * mount((host) => <Dialog _open={host.open} onClose={host.closed}>…</Dialog>);\n *\n * 对话框渲染到 body 末尾,关上后等离场过渡播完再卸载;调用的组件卸载时(例如切换了路由)把开着的对话框关上,\n * 照常走关闭的流程,等着结果的 `await confirm()` 也拿到结果。\n * useDetachedOverlay 在 useDetachedDialog 上面加一层句柄(close、update、closed),useDialog、useDrawer 用它。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport {\n getCurrentInstance,\n getCurrentScope,\n h,\n onScopeDispose,\n render,\n shallowRef,\n type AppContext,\n type VNodeChild,\n} from 'vue';\nimport { ConfigContext, useConfig, type UiConfig } from '../config/context';\nimport { ConfigScope } from '../config/Provider';\nimport type { OpenChangeProps, OpenChangeReasons } from '../utils/dom/open-change';\nimport { afterTransitions } from '../utils/style/transitions';\n\n/** 交给对话框内容的控制:开着没有、关上了 */\nexport interface DetachedDialogHost {\n /** 对话框开着:对话框写 _open={host.open},改成 false 就关上 */\n open: boolean;\n /** 对话框关上了(原生 close 事件里调用):等离场过渡播完卸载 */\n closed: () => void;\n}\n\n/** 组件树之外的根数到第几个了:给每个根一个自己的 useId 命名段 */\nlet detachedRoots = 0;\n\n/**\n * 组件树之外的根:提供调用处的配置,内容包在 ConfigScope 里(主题变量、语言)。\n * render() 出来的根,useId 从头计数、前缀又和应用相同(共用 appContext),生成的 id 会和页面上已有的撞上\n * (表单对话框的 label 关联到页面里另一个控件)。照 Vue 处理异步边界的做法(markAsyncBoundary)换一个命名段:\n * 子组件沿用父组件的 ids,所以在根上换一次就够了。\n */\nconst DetachedRoot = _component(\n ({ config, content }: { config: UiConfig; content: () => VNodeChild }) => {\n const instance = getCurrentInstance() as { ids?: [string, number, number] } | null;\n if (instance?.ids) instance.ids = [`d${++detachedRoots}-`, 0, 0];\n ConfigContext.provide(config);\n return () => <ConfigScope>{content()}</ConfigScope>;\n },\n);\n\n/** 调用处的配置与应用上下文(在 setup 里取),交给组件树之外的根 */\nfunction captureContext() {\n return { config: useConfig(), appContext: getCurrentInstance()?.appContext ?? null };\n}\n\nfunction renderInto(\n container: Element,\n { config, appContext }: { config: UiConfig; appContext: AppContext | null },\n content: () => VNodeChild,\n) {\n const vnode = h(DetachedRoot, { config, content });\n vnode.appContext = appContext;\n render(vnode, container);\n}\n\n/** 在 setup 里调用,返回 renderDetached(content, container):渲染到组件树之外的元素里,调用的组件卸载时一起卸载 */\nexport function useDetachedRender(): (content: VNodeChild | null, container: Element) => void {\n const context = captureContext();\n const containers = new Set<Element>();\n if (getCurrentScope()) {\n onScopeDispose(() => {\n for (const container of containers) render(null, container);\n containers.clear();\n });\n }\n return (content, container) => {\n if (content == null) {\n containers.delete(container);\n render(null, container);\n return;\n }\n containers.add(container);\n renderInto(container, context, () => content);\n };\n}\n\n/** 在 setup 里调用,返回 mount(content):把对话框渲染到组件树之外 */\nexport function useDetachedDialog(): (content: (host: DetachedDialogHost) => VNodeChild) => void {\n const context = captureContext();\n /** 开着的对话框:调用的组件卸载时关上 */\n const live = new Set<DetachedDialogHost>();\n if (getCurrentScope()) {\n onScopeDispose(() => {\n for (const host of live) host.open = false;\n });\n }\n\n return (content) => {\n const container = document.createElement('div');\n document.body.append(container);\n const open = shallowRef(true);\n const host: DetachedDialogHost = {\n get open() {\n return open.value;\n },\n set open(value) {\n open.value = value;\n },\n closed() {\n live.delete(host);\n const dialog = container.querySelector('dialog');\n void (dialog ? afterTransitions(dialog) : Promise.resolve()).then(() => {\n render(null, container);\n container.remove();\n });\n },\n };\n live.add(host);\n renderInto(container, context, () => content(host));\n };\n}\n\n/** 命令式浮层(useDialog、useDrawer)的选项:浮层的属性(_open 由句柄管理),content 是正文 */\nexport type DetachedOverlayOptions<P> = Omit<P, '_open' | 'children'> & {\n /** 正文;给函数时拿到句柄,正文里的按钮可以自己关上、改标题 */\n content?: VNodeChild | ((handle: DetachedOverlayHandle<P>) => VNodeChild);\n};\n\n/** useDialog().open、useDrawer().open 返回的句柄;方法不依赖 this,可以直接传出去(onSaved={handle.close}) */\nexport interface DetachedOverlayHandle<P> {\n /** 关上(onOpenChange 的原因是 imperative);已经关上时什么也不做 */\n readonly close: () => void;\n /** 改属性(标题、宽度、确认按钮的文字、正文……),没写的保持不变 */\n readonly update: (patch: Partial<DetachedOverlayOptions<P>>) => void;\n /** 关上之后 resolve,得到关上的原因:确认是 confirm,Esc、遮罩、关闭按钮、close() 各是 escape、outside、close、imperative */\n readonly closed: Promise<keyof OpenChangeReasons>;\n}\n\n/** 原生 dialog 的 close 事件 */\ntype DialogCloseEvent = Event & { currentTarget: HTMLDialogElement };\n\n/** 浮层要支持的:onOpenChange 报告原因,onClose 落到原生 dialog 上 */\ninterface DetachedOverlayProps extends OpenChangeProps {\n onClose?: (event: DialogCloseEvent) => void;\n}\n\n/**\n * 命令式浮层:在 setup 里调用,返回 open(options)。renderOverlay 把属性、开关与正文交给具体的浮层(Dialog、Drawer):\n *\n * useDetachedOverlay<DialogProps>((props, host, children) => <Dialog {...props} _open={host.open}>{children}</Dialog>);\n */\nexport function useDetachedOverlay<P extends DetachedOverlayProps>(\n renderOverlay: (\n props: Omit<P, '_open' | 'children'>,\n host: DetachedDialogHost,\n children: VNodeChild,\n ) => VNodeChild,\n): (options: DetachedOverlayOptions<P>) => DetachedOverlayHandle<P> {\n const mount = useDetachedDialog();\n return (options) => {\n const state = shallowRef(options);\n let host: DetachedDialogHost | undefined;\n let reason: keyof OpenChangeReasons = 'imperative';\n let resolveClosed!: (reason: keyof OpenChangeReasons) => void;\n const handle: DetachedOverlayHandle<P> = {\n close: () => {\n if (!host?.open) return;\n reason = 'imperative';\n host.open = false;\n state.value.onOpenChange?.(false, { reason });\n },\n update: (patch) => {\n state.value = { ...state.value, ...patch };\n },\n closed: new Promise((resolve) => (resolveClosed = resolve)),\n };\n mount((current) => {\n host = current;\n const { content, onOpenChange, onClose, ...props } = state.value;\n const overlayProps = {\n ...props,\n onOpenChange: (open, details) => {\n if (!open) reason = details.reason;\n onOpenChange?.(open, details);\n },\n onClose: (event: DialogCloseEvent) => {\n onClose?.(event);\n // 原生 close 事件先到这里、后到浮层自己(它在那里报告原因):等它报告完再 resolve\n queueMicrotask(() => resolveClosed(reason));\n current.closed();\n },\n } as Omit<P, '_open' | 'children'> & DetachedOverlayProps;\n return renderOverlay(\n overlayProps,\n current,\n typeof content === 'function' ? content(handle) : content,\n );\n });\n options.onOpenChange?.(true, { reason: 'imperative' });\n return handle;\n };\n}\n"],"mappings":";;;;;;;;AAuCA,IAAI,gBAAgB;;;;;;;AAQpB,IAAM,eAAe,0BACnB,QAAA,SAA0E;CAAA,MAAA,SAAAA,aAAA,QAAA,QAAA;CAAA,MAAA,UAAAA,aAAA,QAAA,SAAA;CACxE,MAAM,WAAW,mBAAmB;CACpC,IAAI,UAAU,KAAK,SAAS,MAAM;EAAC,IAAI,EAAE,cAAa;EAAK;EAAG;CAAC;CAC/D,cAAc,QAAO,OAAA,KAAO;CAC5B,aAAOC,YAAA,aAAA,EAAA,OAAA,UAAA,GAAA;EAAA,eAAA,CAAoB,QAAA,MAAQ,CAAC;EAAA,SAAA;CAAA,CAAA;AACtC,GAAC;CAAA,MAAA;CAAA,OAAA,CAAA,UAAA,SAAA;AAAA,CACH;;AAGA,SAAS,iBAAiB;CACxB,OAAO;EAAE,QAAQ,UAAU;EAAG,YAAY,mBAAmB,CAAC,EAAE,cAAc;CAAK;AACrF;AAEA,SAAS,WACP,WACA,EAAE,QAAQ,cACV,SACA;CACA,MAAM,QAAQ,EAAE,cAAc;EAAE;EAAQ;CAAQ,CAAC;CACjD,MAAM,aAAa;CACnB,OAAO,OAAO,SAAS;AACzB;;AAGA,SAAgB,oBAA8E;CAC5F,MAAM,UAAU,eAAe;CAC/B,MAAM,6BAAa,IAAI,IAAa;CACpC,IAAI,gBAAgB,GAClB,qBAAqB;EACnB,KAAK,MAAM,aAAa,YAAY,OAAO,MAAM,SAAS;EAC1D,WAAW,MAAM;CACnB,CAAC;CAEH,QAAQ,SAAS,cAAc;EAC7B,IAAI,WAAW,MAAM;GACnB,WAAW,OAAO,SAAS;GAC3B,OAAO,MAAM,SAAS;GACtB;EACF;EACA,WAAW,IAAI,SAAS;EACxB,WAAW,WAAW,eAAe,OAAO;CAC9C;AACF;;AAGA,SAAgB,oBAAiF;CAC/F,MAAM,UAAU,eAAe;;CAE/B,MAAM,uBAAO,IAAI,IAAwB;CACzC,IAAI,gBAAgB,GAClB,qBAAqB;EACnB,KAAK,MAAM,QAAQ,MAAM,KAAK,OAAO;CACvC,CAAC;CAGH,QAAQ,YAAY;EAClB,MAAM,YAAY,SAAS,cAAc,KAAK;EAC9C,SAAS,KAAK,OAAO,SAAS;EAC9B,MAAM,OAAO,WAAW,IAAI;EAC5B,MAAM,OAA2B;GAC/B,IAAI,OAAO;IACT,OAAO,KAAK;GACd;GACA,IAAI,KAAK,OAAO;IACd,KAAK,QAAQ;GACf;GACA,SAAS;IACP,KAAK,OAAO,IAAI;IAChB,MAAM,SAAS,UAAU,cAAc,QAAQ;IAC/C,CAAM,SAAS,iBAAiB,MAAM,IAAI,QAAQ,QAAQ,EAAA,CAAG,WAAW;KACtE,OAAO,MAAM,SAAS;KACtB,UAAU,OAAO;IACnB,CAAC;GACH;EACF;EACA,KAAK,IAAI,IAAI;EACb,WAAW,WAAW,eAAe,QAAQ,IAAI,CAAC;CACpD;AACF;;;;;;;;;;AA+BA,SAAgB,mBACd,eAKkE;CAClE,MAAM,QAAQ,kBAAkB;CAChC,QAAQ,YAAY;EAClB,MAAM,QAAQ,WAAW,OAAO;EAChC,IAAI;EACJ,IAAI,SAAkC;EACtC,IAAI;EACJ,MAAM,SAAmC;GACvC,aAAa;IACX,IAAI,CAAC,MAAM,MAAM;IACjB,SAAS;IACT,KAAK,OAAO;IACZ,MAAM,MAAM,eAAe,OAAO,EAAE,OAAO,CAAC;GAC9C;GACA,SAAS,UAAU;IACjB,MAAM,QAAQ;KAAE,GAAG,MAAM;KAAO,GAAG;IAAM;GAC3C;GACA,QAAQ,IAAI,SAAS,YAAa,gBAAgB,OAAQ;EAC5D;EACA,OAAO,YAAY;GACjB,OAAO;GACP,MAAM,EAAE,SAAS,cAAc,SAAS,GAAG,UAAU,MAAM;GAc3D,OAAO,cACL;IAbA,GAAG;IACH,eAAe,MAAM,YAAY;KAC/B,IAAI,CAAC,MAAM,SAAS,QAAQ;KAC5B,eAAe,MAAM,OAAO;IAC9B;IACA,UAAU,UAA4B;KACpC,UAAU,KAAK;KAEf,qBAAqB,cAAc,MAAM,CAAC;KAC1C,QAAQ,OAAO;IACjB;GAGA,GACA,SACA,OAAO,YAAY,aAAa,QAAQ,MAAM,IAAI,OACpD;EACF,CAAC;EACD,QAAQ,eAAe,MAAM,EAAE,QAAQ,aAAa,CAAC;EACrD,OAAO;CACT;AACF"}
|
|
@@ -161,7 +161,6 @@ var QueryTable = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
161
161
|
createVNode(IconButton, toolButton({
|
|
162
162
|
icon: RefreshCw,
|
|
163
163
|
"aria-label": messages.queryTableRefresh,
|
|
164
|
-
loading: query.value.loading,
|
|
165
164
|
onClick: () => void query.value.refresh()
|
|
166
165
|
}), null),
|
|
167
166
|
createVNode(Menu$1, {
|
|
@@ -221,7 +220,6 @@ var QueryTable = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
221
220
|
return createVNode("div", mergeAttrs({ sx: [queryTableStyles.root, fill.value && queryTableStyles.rootFill] }, rest), [filters.value && createVNode(FilterBar, mergePartProps({ "aria-label": messages.queryTableSearch(title.value) }, propsFilterBar.value, {
|
|
222
221
|
onSearch: search,
|
|
223
222
|
onReset: reset,
|
|
224
|
-
loading: query.value.loading,
|
|
225
223
|
more: moreFilters.value?.(draft),
|
|
226
224
|
start: fullscreen.active ? void 0 : createVNode("div", css.attrs(queryTableStyles.titleInline), [title.value]),
|
|
227
225
|
end: fullscreen.active ? void 0 : createVNode("div", mergeAttrs({ sx: queryTableStyles.toolbarInline }, propsToolbar.value), [toolbar.value, renderTools()])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryTable.js","names":["_isVNode","_vtp_readonlyProp","_vtp_bindable","_vtp_restProps","_vtp_untrack","_vue_shallowRef","_createVNode","Menu","_mergeProps"],"sources":["../../src/patterns/QueryTable.tsx"],"sourcesContent":["/**\n * 查询表格(列表页的整块):查询栏(FilterBar)+ 工具栏(标题、自己的按钮、刷新、行高、列设置、全屏)+ 批量操作栏 + 数据表格 + 分页栏。\n * 有查询栏时工具栏并进查询栏那一行,压成一行:标题 条件 查询 重置 展开 … 自己的按钮 刷新 行高 列设置 全屏;\n * 条件多时把不常用的放进 moreFilters,点“展开”后更多条件排在这一行下面。没有查询栏、或全屏(全屏的只有表格那一块)时,\n * 工具栏是表格上方单独的一行。fill 时整块占满到窗口底边(ViewportFill),行多了在表格里滚动,分页留在一屏里。\n * 状态都在 useTableQuery 里(服务端分页、排序、条件)。同 Ant Design ProComponents 的 ProTable。\n *\n * const tasks = useTableQuery(fetchTasks, { filters: { keyword: '', status: '' }, pageSize: 20 });\n * <QueryTable\n * query={tasks} columns={columns} getKey={(task) => task.id} title=\"任务台账\" persistKey=\"tasks\"\n * filters={(draft) => <Field label=\"关键字\"><TextField _value={draft.keyword} /></Field>}\n * toolbar={<Button icon={Plus}>新建</Button>}\n * />\n *\n * - 查询栏的条件改的是草稿(draft),点“查询”(或回车)才交给 query.search;“重置”回到 blankFilters(不写时是一开始的条件)并查询。\n * - 表头排序交给服务端(manualSort,写回 query.sort;multiSort 时按住 Shift 追加一列,写回 query.sorts),\n * 表头筛选也交给服务端(manualFilter,写回 query.columnFilters;列的 filter 要写 options,否则只列出当前这页出现过的值),\n * 分页栏绑定 query.page / pageSize / total。fetcher 用 tableParams(query, columns) 把这些写成接口的参数。\n * - persistKey:列设置(显示、顺序、固定、列宽)与行高记在浏览器里,下次打开还是这样。\n * - selectionMode 时勾选行,表格上方出现批量操作栏,batchActions 拿到已选的 key 给出按钮。\n *\n * 部件:Root(div)、FilterBar、Panel(工具栏与表格所在的区域,全屏的就是它)、Toolbar(div)、Title(div)、\n * Table(DataTable)、SelectionBar、Pagination(PaginationBar)。\n */\nimport { Maximize, Minimize, RefreshCw, Rows3 } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { reactive, shallowRef, type Ref, type VNodeChild } from 'vue';\nimport { Button } from '../base/Button';\nimport { IconButton, type IconButtonProps } from '../base/IconButton';\nimport { useLang } from '../config/context';\nimport { useMessages } from '../config/lang/messages';\nimport { Alert, type AlertProps } from '../feedback/Alert';\nimport { PaginationBar, type PaginationBarProps } from '../navigation/PaginationBar';\nimport { Menu, MenuRadioGroup, MenuRadioItem } from '../overlay/Menu';\nimport { ColumnSettings } from '../table/ColumnSettings';\nimport type { ColumnStickies, DataTableColumn } from '../table/columns';\nimport { ViewportFill } from '../layout/ViewportFill';\nimport { DataTable, type DataTableProps } from '../table/DataTable';\nimport { SelectionBar, type SelectionBarProps } from '../table/SelectionBar';\nimport type { DataTableDensities } from '../table/table-styles';\nimport type { TablePage, TableQueryState } from '../table/use-table-query';\nimport { cloneValue } from '../utils/data/clone';\nimport { useFullscreen } from '../utils/dom/fullscreen';\nimport { usePersistentState } from '../utils/dom/storage';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { errorMessage } from '../utils/request/error-message';\nimport { FilterBar, type FilterBarProps } from './FilterBar';\nimport { patternsMessages } from './messages';\n\n/** 未声明的属性落到外层 div 上 */\nexport interface QueryTableProps<T, Filters extends object> extends PartProps<'div'> {\n /** 列表状态:useTableQuery 的返回值 */\n query: TableQueryState<TablePage<T>, Filters>;\n /** 列定义 */\n columns: readonly DataTableColumn<T>[];\n /** 行的 key */\n getKey: (row: T, index: number) => string;\n /** 表格的名称(工具栏起始边的标题,也是表格的 aria-label) */\n title: string;\n /** 查询栏的条件:拿到草稿,控件绑定它的字段(`_value={draft.keyword}`);不写时没有查询栏 */\n filters?: (draft: Filters) => VNodeChild;\n /** 收起的条件(不常用的),点“展开”才显示 */\n moreFilters?: (draft: Filters) => VNodeChild;\n /** “重置”回到的条件;不写时是 query 一开始的条件 */\n blankFilters?: Filters;\n /** 占满到窗口底边(列表页的整页):行多了在表格里滚动,分页一直在一屏里 */\n fill?: boolean;\n /** 工具栏上自己的按钮(新建、导出),在标题之后、内置按钮之前 */\n toolbar?: VNodeChild;\n /** 列设置与行高记在浏览器里的键(localStorage,自动加前缀);不写时只在这次打开里有效 */\n persistKey?: string;\n /** 多列排序:按住 Shift 点列标题追加一列(同 DataTable 的 multiSort),排序写在 query.sorts */\n multiSort?: boolean;\n /** 勾选行:多选时表格上方出现批量操作栏 */\n selectionMode?: DataTableProps<T>['selectionMode'];\n /** 已选行的 key:父级写 _selected={keys} */\n _selected?: string[];\n /** 批量操作栏里的按钮:拿到已选的 key */\n batchActions?: (keys: readonly string[]) => VNodeChild;\n /** 查询栏(FilterBar)的部件属性 */\n propsFilterBar?: Partial<FilterBarProps>;\n /** 工具栏与表格所在区域的部件属性(全屏的就是它) */\n propsPanel?: PartProps<'div'>;\n /** 工具栏的部件属性 */\n propsToolbar?: PartProps<'div'>;\n /** 表格(DataTable)的部件属性:列宽、详情行、行的色调等直接写在这里 */\n propsTable?: Partial<DataTableProps<T>>;\n /** 批量操作栏的部件属性 */\n propsSelectionBar?: Partial<SelectionBarProps>;\n /** 分页栏的部件属性 */\n propsPagination?: Partial<PaginationBarProps>;\n /** 加载失败时表格上方的提示条(Alert)的部件属性 */\n propsError?: Partial<AlertProps>;\n /** 工具栏内置按钮(刷新、行高、全屏)的部件属性 */\n propsToolbarButton?: Partial<IconButtonProps>;\n}\n\n/** QueryTable 的样式:root、rootFill、panel、panelFullscreen、panelFill、toolbar、title、tools、titleInline、toolbarInline */\nexport const queryTableStyles = css.create({\n root: { display: 'flex', flexDirection: 'column', gap: '_md', minWidth: 0 },\n panel: {\n display: 'flex',\n flexDirection: 'column',\n gap: '_sm',\n minWidth: 0,\n backgroundColor: '_surface',\n },\n // 全屏时铺满屏幕、四周留白,表格占满剩下的高度\n panelFullscreen: { padding: '_lg', overflow: 'auto' },\n // fill 时外面的高度定了,整块与区域可以压矮,压不下的在表格里滚动\n rootFill: { flexGrow: 1, minHeight: 0 },\n panelFill: { minHeight: 0 },\n toolbar: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '_sm' },\n title: { flexGrow: 1, minWidth: 0, fontWeight: '_semibold', fontSize: '_lg' },\n tools: { display: 'flex', alignItems: 'center', gap: '_xs' },\n // 并进查询栏那一行时:标题不撑开,按钮靠在一起\n titleInline: { flexShrink: 0, fontWeight: '_semibold', fontSize: '_lg' },\n toolbarInline: { display: 'flex', alignItems: 'center', gap: '_sm' },\n});\n\n/** 存在浏览器里(给了 persistKey)或只在这次打开里有效的状态 */\nfunction settingRef<V>(key: string | undefined, name: string, initial: V): Ref<V> {\n return key ? usePersistentState(`${key}:${name}`, initial) : shallowRef(initial);\n}\n\n/** 查询表格:查询栏 + 工具栏 + 表格 + 分页,状态来自 useTableQuery */\nexport const QueryTable = _component(\n <T, Filters extends object>({\n query,\n columns,\n getKey,\n title,\n filters,\n moreFilters,\n blankFilters,\n fill = false,\n toolbar,\n persistKey,\n multiSort = false,\n selectionMode,\n _selected: selected = _bindable<string[]>([]),\n batchActions,\n propsFilterBar,\n propsPanel,\n propsToolbar,\n propsTable,\n propsSelectionBar,\n propsPagination,\n propsError,\n propsToolbarButton,\n ...rest\n }: QueryTableProps<T, Filters>) => {\n const messages = useMessages('patterns', patternsMessages);\n const lang = useLang();\n // 查询栏改的是草稿:点“查询”才交给 query\n // 用 cloneValue 不用 structuredClone:条件里的日期(CalendarDate)、Decimal 要保持原样\n const initial = cloneValue(_untrack(query.filters));\n const draft = reactive({ ...initial }) as Filters;\n const key = _untrack(persistKey);\n let hidden = _fromRef(settingRef<string[]>(key, 'hidden', []));\n let order = _fromRef(settingRef<string[]>(key, 'order', []));\n let pins = _fromRef(settingRef<Record<string, keyof ColumnStickies | null>>(key, 'pins', {}));\n let widths = _fromRef(settingRef<Record<string, number>>(key, 'widths', {}));\n let density = _fromRef(settingRef<keyof DataTableDensities>(key, 'density', 'standard'));\n let panel = _state.raw<HTMLElement | null>(null);\n const fullscreen = useFullscreen(() => panel);\n\n function search() {\n query.search(cloneValue(draft) as Filters);\n }\n\n function reset() {\n Object.assign(draft, cloneValue(blankFilters ?? initial));\n search();\n }\n\n const toolButton = (props: Partial<IconButtonProps>) =>\n mergePartProps<IconButtonProps>(\n { icon: RefreshCw, variant: 'ghost', tone: 'neutral', 'aria-label': '' },\n propsToolbarButton,\n props,\n );\n\n function renderTools() {\n return (\n <div {...css.attrs(queryTableStyles.tools)}>\n <IconButton\n {...toolButton({\n icon: RefreshCw,\n 'aria-label': messages.queryTableRefresh,\n loading: query.loading,\n onClick: () => void query.refresh(),\n })}\n />\n <Menu\n placement=\"bottom-end\"\n trigger={(props) => (\n <IconButton\n {...toolButton({ icon: Rows3, 'aria-label': messages.queryTableDensity })}\n {...props}\n />\n )}\n >\n <MenuRadioGroup\n _value={density}\n onUpdate:_value={(value: string) => (density = value as keyof DataTableDensities)}\n >\n <MenuRadioItem value=\"compact\">{messages.queryTableDensityCompact}</MenuRadioItem>\n <MenuRadioItem value=\"standard\">{messages.queryTableDensityStandard}</MenuRadioItem>\n <MenuRadioItem value=\"comfortable\">\n {messages.queryTableDensityComfortable}\n </MenuRadioItem>\n </MenuRadioGroup>\n </Menu>\n <ColumnSettings\n columns={columns}\n _hidden={hidden}\n _order={order}\n onReset={() => {\n pins = {};\n widths = {};\n }}\n propsTrigger={propsToolbarButton}\n />\n {fullscreen.supported && (\n <IconButton\n {...toolButton({\n icon: fullscreen.active ? Minimize : Maximize,\n 'aria-label': fullscreen.active\n ? messages.queryTableExitFullscreen\n : messages.queryTableFullscreen,\n 'aria-pressed': fullscreen.active,\n onClick: () => void fullscreen.toggle(),\n })}\n />\n )}\n </div>\n );\n }\n\n return () => {\n const root = renderRoot();\n return fill ? <ViewportFill>{root}</ViewportFill> : root;\n };\n\n function renderRoot() {\n return (\n <div\n {...mergeAttrs<'div'>(\n { sx: [queryTableStyles.root, fill && queryTableStyles.rootFill] },\n rest,\n )}\n >\n {filters && (\n <FilterBar\n {...mergePartProps<Partial<FilterBarProps>>(\n // 一页上可能有几个查询栏:各有名称,search 地标才分得清\n { 'aria-label': messages.queryTableSearch(title) },\n propsFilterBar,\n {\n onSearch: search,\n onReset: reset,\n loading: query.loading,\n more: moreFilters?.(draft),\n // 全屏时只有表格那一块(查询栏不在里面),工具栏要留在表格上方,所以不并进来\n start: fullscreen.active ? undefined : (\n <div {...css.attrs(queryTableStyles.titleInline)}>{title}</div>\n ),\n end: fullscreen.active ? undefined : (\n <div\n {...mergeAttrs<'div'>({ sx: queryTableStyles.toolbarInline }, propsToolbar)}\n >\n {toolbar}\n {renderTools()}\n </div>\n ),\n },\n )}\n >\n {filters(draft)}\n </FilterBar>\n )}\n <div\n {...mergeAttrs<'div'>(\n {\n sx: [\n queryTableStyles.panel,\n fill && queryTableStyles.panelFill,\n fullscreen.active && queryTableStyles.panelFullscreen,\n ],\n },\n propsPanel,\n { ref: _toRef(panel) },\n )}\n >\n {(!filters || fullscreen.active) && (\n <div {...mergeAttrs<'div'>({ sx: queryTableStyles.toolbar }, propsToolbar)}>\n <div {...css.attrs(queryTableStyles.title)}>{title}</div>\n {toolbar}\n {renderTools()}\n </div>\n )}\n {selectionMode === 'multiple' && (\n <SelectionBar\n {...mergePartProps<SelectionBarProps>(\n { count: selected.length },\n propsSelectionBar,\n {\n count: selected.length,\n onClear: () => (selected = []),\n },\n )}\n >\n {batchActions?.(selected)}\n </SelectionBar>\n )}\n {/* 加载失败:按错误码说明原因(网络、权限、条件写错),保留上一次的行,给一个重试 */}\n {query.error !== undefined && (\n <Alert\n {...mergePartProps<Partial<AlertProps>>(\n {\n tone: 'danger',\n live: 'assertive',\n action: (\n <Button\n size=\"sm\"\n variant=\"outline\"\n tone=\"danger\"\n onClick={() => void query.refresh()}\n >\n {messages.queryTableRetry}\n </Button>\n ),\n },\n propsError,\n )}\n >\n {errorMessage(query.error, lang)}\n </Alert>\n )}\n <DataTable\n {...mergePartProps<Partial<DataTableProps<T>>>(\n { 'aria-label': title, flexible: fill || fullscreen.active },\n propsTable,\n )}\n rows={query.rows as T[]}\n columns={columns}\n getKey={getKey}\n manualSort\n multiSort={multiSort}\n _sort={query.sort}\n _sorts={query.sorts}\n manualFilter\n _filters={query.columnFilters}\n loading={query.loading}\n density={density}\n selectionMode={selectionMode}\n _selected={selected}\n _hiddenColumns={hidden}\n _columnOrder={order}\n _columnPins={pins}\n _columnWidths={widths}\n resizable\n columnMenu\n />\n <PaginationBar\n {...mergePartProps<Partial<PaginationBarProps>>(\n { propsPagination: { 'aria-label': messages.queryTablePagination(title) } },\n propsPagination,\n )}\n _page={query.page}\n _pageSize={query.pageSize}\n total={query.total}\n />\n </div>\n </div>\n );\n }\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QueryTable.js","names":["_isVNode","_vtp_readonlyProp","_vtp_bindable","_vtp_restProps","_vtp_untrack","_vue_shallowRef","_createVNode","Menu","_mergeProps"],"sources":["../../src/patterns/QueryTable.tsx"],"sourcesContent":["/**\n * 查询表格(列表页的整块):查询栏(FilterBar)+ 工具栏(标题、自己的按钮、刷新、行高、列设置、全屏)+ 批量操作栏 + 数据表格 + 分页栏。\n * 有查询栏时工具栏并进查询栏那一行,压成一行:标题 条件 查询 重置 展开 … 自己的按钮 刷新 行高 列设置 全屏;\n * 条件多时把不常用的放进 moreFilters,点“展开”后更多条件排在这一行下面。没有查询栏、或全屏(全屏的只有表格那一块)时,\n * 工具栏是表格上方单独的一行。fill 时整块占满到窗口底边(ViewportFill),行多了在表格里滚动,分页留在一屏里。\n * 状态都在 useTableQuery 里(服务端分页、排序、条件)。同 Ant Design ProComponents 的 ProTable。\n *\n * const tasks = useTableQuery(fetchTasks, { filters: { keyword: '', status: '' }, pageSize: 20 });\n * <QueryTable\n * query={tasks} columns={columns} getKey={(task) => task.id} title=\"任务台账\" persistKey=\"tasks\"\n * filters={(draft) => <Field label=\"关键字\"><TextField _value={draft.keyword} /></Field>}\n * toolbar={<Button icon={Plus}>新建</Button>}\n * />\n *\n * - 查询栏的条件改的是草稿(draft),点“查询”(或回车)才交给 query.search;“重置”回到 blankFilters(不写时是一开始的条件)并查询。\n * - 表头排序交给服务端(manualSort,写回 query.sort;multiSort 时按住 Shift 追加一列,写回 query.sorts),\n * 表头筛选也交给服务端(manualFilter,写回 query.columnFilters;列的 filter 要写 options,否则只列出当前这页出现过的值),\n * 分页栏绑定 query.page / pageSize / total。fetcher 用 tableParams(query, columns) 把这些写成接口的参数。\n * - persistKey:列设置(显示、顺序、固定、列宽)与行高记在浏览器里,下次打开还是这样。\n * - selectionMode 时勾选行,表格上方出现批量操作栏,batchActions 拿到已选的 key 给出按钮。\n *\n * 部件:Root(div)、FilterBar、Panel(工具栏与表格所在的区域,全屏的就是它)、Toolbar(div)、Title(div)、\n * Table(DataTable)、SelectionBar、Pagination(PaginationBar)。\n */\nimport { Maximize, Minimize, RefreshCw, Rows3 } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { reactive, shallowRef, type Ref, type VNodeChild } from 'vue';\nimport { Button } from '../base/Button';\nimport { IconButton, type IconButtonProps } from '../base/IconButton';\nimport { useLang } from '../config/context';\nimport { useMessages } from '../config/lang/messages';\nimport { Alert, type AlertProps } from '../feedback/Alert';\nimport { PaginationBar, type PaginationBarProps } from '../navigation/PaginationBar';\nimport { Menu, MenuRadioGroup, MenuRadioItem } from '../overlay/Menu';\nimport { ColumnSettings } from '../table/ColumnSettings';\nimport type { ColumnStickies, DataTableColumn } from '../table/columns';\nimport { ViewportFill } from '../layout/ViewportFill';\nimport { DataTable, type DataTableProps } from '../table/DataTable';\nimport { SelectionBar, type SelectionBarProps } from '../table/SelectionBar';\nimport type { DataTableDensities } from '../table/table-styles';\nimport type { TablePage, TableQueryState } from '../table/use-table-query';\nimport { cloneValue } from '../utils/data/clone';\nimport { useFullscreen } from '../utils/dom/fullscreen';\nimport { usePersistentState } from '../utils/dom/storage';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { errorMessage } from '../utils/request/error-message';\nimport { FilterBar, type FilterBarProps } from './FilterBar';\nimport { patternsMessages } from './messages';\n\n/** 未声明的属性落到外层 div 上 */\nexport interface QueryTableProps<T, Filters extends object> extends PartProps<'div'> {\n /** 列表状态:useTableQuery 的返回值 */\n query: TableQueryState<TablePage<T>, Filters>;\n /** 列定义 */\n columns: readonly DataTableColumn<T>[];\n /** 行的 key */\n getKey: (row: T, index: number) => string;\n /** 表格的名称(工具栏起始边的标题,也是表格的 aria-label) */\n title: string;\n /** 查询栏的条件:拿到草稿,控件绑定它的字段(`_value={draft.keyword}`);不写时没有查询栏 */\n filters?: (draft: Filters) => VNodeChild;\n /** 收起的条件(不常用的),点“展开”才显示 */\n moreFilters?: (draft: Filters) => VNodeChild;\n /** “重置”回到的条件;不写时是 query 一开始的条件 */\n blankFilters?: Filters;\n /** 占满到窗口底边(列表页的整页):行多了在表格里滚动,分页一直在一屏里 */\n fill?: boolean;\n /** 工具栏上自己的按钮(新建、导出),在标题之后、内置按钮之前 */\n toolbar?: VNodeChild;\n /** 列设置与行高记在浏览器里的键(localStorage,自动加前缀);不写时只在这次打开里有效 */\n persistKey?: string;\n /** 多列排序:按住 Shift 点列标题追加一列(同 DataTable 的 multiSort),排序写在 query.sorts */\n multiSort?: boolean;\n /** 勾选行:多选时表格上方出现批量操作栏 */\n selectionMode?: DataTableProps<T>['selectionMode'];\n /** 已选行的 key:父级写 _selected={keys} */\n _selected?: string[];\n /** 批量操作栏里的按钮:拿到已选的 key */\n batchActions?: (keys: readonly string[]) => VNodeChild;\n /** 查询栏(FilterBar)的部件属性 */\n propsFilterBar?: Partial<FilterBarProps>;\n /** 工具栏与表格所在区域的部件属性(全屏的就是它) */\n propsPanel?: PartProps<'div'>;\n /** 工具栏的部件属性 */\n propsToolbar?: PartProps<'div'>;\n /** 表格(DataTable)的部件属性:列宽、详情行、行的色调等直接写在这里 */\n propsTable?: Partial<DataTableProps<T>>;\n /** 批量操作栏的部件属性 */\n propsSelectionBar?: Partial<SelectionBarProps>;\n /** 分页栏的部件属性 */\n propsPagination?: Partial<PaginationBarProps>;\n /** 加载失败时表格上方的提示条(Alert)的部件属性 */\n propsError?: Partial<AlertProps>;\n /** 工具栏内置按钮(刷新、行高、全屏)的部件属性 */\n propsToolbarButton?: Partial<IconButtonProps>;\n}\n\n/** QueryTable 的样式:root、rootFill、panel、panelFullscreen、panelFill、toolbar、title、tools、titleInline、toolbarInline */\nexport const queryTableStyles = css.create({\n root: { display: 'flex', flexDirection: 'column', gap: '_md', minWidth: 0 },\n panel: {\n display: 'flex',\n flexDirection: 'column',\n gap: '_sm',\n minWidth: 0,\n backgroundColor: '_surface',\n },\n // 全屏时铺满屏幕、四周留白,表格占满剩下的高度\n panelFullscreen: { padding: '_lg', overflow: 'auto' },\n // fill 时外面的高度定了,整块与区域可以压矮,压不下的在表格里滚动\n rootFill: { flexGrow: 1, minHeight: 0 },\n panelFill: { minHeight: 0 },\n toolbar: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '_sm' },\n title: { flexGrow: 1, minWidth: 0, fontWeight: '_semibold', fontSize: '_lg' },\n tools: { display: 'flex', alignItems: 'center', gap: '_xs' },\n // 并进查询栏那一行时:标题不撑开,按钮靠在一起\n titleInline: { flexShrink: 0, fontWeight: '_semibold', fontSize: '_lg' },\n toolbarInline: { display: 'flex', alignItems: 'center', gap: '_sm' },\n});\n\n/** 存在浏览器里(给了 persistKey)或只在这次打开里有效的状态 */\nfunction settingRef<V>(key: string | undefined, name: string, initial: V): Ref<V> {\n return key ? usePersistentState(`${key}:${name}`, initial) : shallowRef(initial);\n}\n\n/** 查询表格:查询栏 + 工具栏 + 表格 + 分页,状态来自 useTableQuery */\nexport const QueryTable = _component(\n <T, Filters extends object>({\n query,\n columns,\n getKey,\n title,\n filters,\n moreFilters,\n blankFilters,\n fill = false,\n toolbar,\n persistKey,\n multiSort = false,\n selectionMode,\n _selected: selected = _bindable<string[]>([]),\n batchActions,\n propsFilterBar,\n propsPanel,\n propsToolbar,\n propsTable,\n propsSelectionBar,\n propsPagination,\n propsError,\n propsToolbarButton,\n ...rest\n }: QueryTableProps<T, Filters>) => {\n const messages = useMessages('patterns', patternsMessages);\n const lang = useLang();\n // 查询栏改的是草稿:点“查询”才交给 query\n // 用 cloneValue 不用 structuredClone:条件里的日期(CalendarDate)、Decimal 要保持原样\n const initial = cloneValue(_untrack(query.filters));\n const draft = reactive({ ...initial }) as Filters;\n const key = _untrack(persistKey);\n let hidden = _fromRef(settingRef<string[]>(key, 'hidden', []));\n let order = _fromRef(settingRef<string[]>(key, 'order', []));\n let pins = _fromRef(settingRef<Record<string, keyof ColumnStickies | null>>(key, 'pins', {}));\n let widths = _fromRef(settingRef<Record<string, number>>(key, 'widths', {}));\n let density = _fromRef(settingRef<keyof DataTableDensities>(key, 'density', 'standard'));\n let panel = _state.raw<HTMLElement | null>(null);\n const fullscreen = useFullscreen(() => panel);\n\n function search() {\n query.search(cloneValue(draft) as Filters);\n }\n\n function reset() {\n Object.assign(draft, cloneValue(blankFilters ?? initial));\n search();\n }\n\n const toolButton = (props: Partial<IconButtonProps>) =>\n mergePartProps<IconButtonProps>(\n { icon: RefreshCw, variant: 'ghost', tone: 'neutral', 'aria-label': '' },\n propsToolbarButton,\n props,\n );\n\n function renderTools() {\n return (\n <div {...css.attrs(queryTableStyles.tools)}>\n <IconButton\n {...toolButton({\n icon: RefreshCw,\n 'aria-label': messages.queryTableRefresh,\n onClick: () => void query.refresh(),\n })}\n />\n <Menu\n placement=\"bottom-end\"\n trigger={(props) => (\n <IconButton\n {...toolButton({ icon: Rows3, 'aria-label': messages.queryTableDensity })}\n {...props}\n />\n )}\n >\n <MenuRadioGroup\n _value={density}\n onUpdate:_value={(value: string) => (density = value as keyof DataTableDensities)}\n >\n <MenuRadioItem value=\"compact\">{messages.queryTableDensityCompact}</MenuRadioItem>\n <MenuRadioItem value=\"standard\">{messages.queryTableDensityStandard}</MenuRadioItem>\n <MenuRadioItem value=\"comfortable\">\n {messages.queryTableDensityComfortable}\n </MenuRadioItem>\n </MenuRadioGroup>\n </Menu>\n <ColumnSettings\n columns={columns}\n _hidden={hidden}\n _order={order}\n onReset={() => {\n pins = {};\n widths = {};\n }}\n propsTrigger={propsToolbarButton}\n />\n {fullscreen.supported && (\n <IconButton\n {...toolButton({\n icon: fullscreen.active ? Minimize : Maximize,\n 'aria-label': fullscreen.active\n ? messages.queryTableExitFullscreen\n : messages.queryTableFullscreen,\n 'aria-pressed': fullscreen.active,\n onClick: () => void fullscreen.toggle(),\n })}\n />\n )}\n </div>\n );\n }\n\n return () => {\n const root = renderRoot();\n return fill ? <ViewportFill>{root}</ViewportFill> : root;\n };\n\n function renderRoot() {\n return (\n <div\n {...mergeAttrs<'div'>(\n { sx: [queryTableStyles.root, fill && queryTableStyles.rootFill] },\n rest,\n )}\n >\n {filters && (\n <FilterBar\n {...mergePartProps<Partial<FilterBarProps>>(\n // 一页上可能有几个查询栏:各有名称,search 地标才分得清\n { 'aria-label': messages.queryTableSearch(title) },\n propsFilterBar,\n {\n onSearch: search,\n onReset: reset,\n more: moreFilters?.(draft),\n // 全屏时只有表格那一块(查询栏不在里面),工具栏要留在表格上方,所以不并进来\n start: fullscreen.active ? undefined : (\n <div {...css.attrs(queryTableStyles.titleInline)}>{title}</div>\n ),\n end: fullscreen.active ? undefined : (\n <div\n {...mergeAttrs<'div'>({ sx: queryTableStyles.toolbarInline }, propsToolbar)}\n >\n {toolbar}\n {renderTools()}\n </div>\n ),\n },\n )}\n >\n {filters(draft)}\n </FilterBar>\n )}\n <div\n {...mergeAttrs<'div'>(\n {\n sx: [\n queryTableStyles.panel,\n fill && queryTableStyles.panelFill,\n fullscreen.active && queryTableStyles.panelFullscreen,\n ],\n },\n propsPanel,\n { ref: _toRef(panel) },\n )}\n >\n {(!filters || fullscreen.active) && (\n <div {...mergeAttrs<'div'>({ sx: queryTableStyles.toolbar }, propsToolbar)}>\n <div {...css.attrs(queryTableStyles.title)}>{title}</div>\n {toolbar}\n {renderTools()}\n </div>\n )}\n {selectionMode === 'multiple' && (\n <SelectionBar\n {...mergePartProps<SelectionBarProps>(\n { count: selected.length },\n propsSelectionBar,\n {\n count: selected.length,\n onClear: () => (selected = []),\n },\n )}\n >\n {batchActions?.(selected)}\n </SelectionBar>\n )}\n {/* 加载失败:按错误码说明原因(网络、权限、条件写错),保留上一次的行,给一个重试 */}\n {query.error !== undefined && (\n <Alert\n {...mergePartProps<Partial<AlertProps>>(\n {\n tone: 'danger',\n live: 'assertive',\n action: (\n <Button\n size=\"sm\"\n variant=\"outline\"\n tone=\"danger\"\n onClick={() => void query.refresh()}\n >\n {messages.queryTableRetry}\n </Button>\n ),\n },\n propsError,\n )}\n >\n {errorMessage(query.error, lang)}\n </Alert>\n )}\n <DataTable\n {...mergePartProps<Partial<DataTableProps<T>>>(\n { 'aria-label': title, flexible: fill || fullscreen.active },\n propsTable,\n )}\n rows={query.rows as T[]}\n columns={columns}\n getKey={getKey}\n manualSort\n multiSort={multiSort}\n _sort={query.sort}\n _sorts={query.sorts}\n manualFilter\n _filters={query.columnFilters}\n // 加载提示只有表格的骨架一处:刷新、查询按钮不再各自转圈\n loading={query.loading}\n density={density}\n selectionMode={selectionMode}\n _selected={selected}\n _hiddenColumns={hidden}\n _columnOrder={order}\n _columnPins={pins}\n _columnWidths={widths}\n resizable\n columnMenu\n />\n <PaginationBar\n {...mergePartProps<Partial<PaginationBarProps>>(\n { propsPagination: { 'aria-label': messages.queryTablePagination(title) } },\n propsPagination,\n )}\n _page={query.page}\n _pageSize={query.pageSize}\n total={query.total}\n />\n </div>\n </div>\n );\n }\n },\n);\n"],"mappings":"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"}
|
package/dist/table/DataTable.js
CHANGED
|
@@ -47,7 +47,7 @@ import { GripVertical } from "@lucide/icons";
|
|
|
47
47
|
* - 选择:selectionMode 为 single / multiple 时多一列勾选框,selected 是已选行的 key。
|
|
48
48
|
* - 列宽:resizable 时可以拖动或用键盘调整,columnWidths 记录调整后的宽度(可以双向绑定,保存到本地设置里)。
|
|
49
49
|
* - 定位:handle 拿到 scrollToRow(key),滚到那一行(停在吸顶表头之下)。
|
|
50
|
-
* -
|
|
50
|
+
* - 加载:只用骨架行:loading 时表体换成骨架;滚动加载(onRangeChange)已有数据时骨架接在末尾。
|
|
51
51
|
* - 详情行:renderDetail 给出一行的详情,多一列展开按钮;expandedRows 是展开了的行(可以双向绑定),hasDetail 决定哪些行有。
|
|
52
52
|
* - 分组表头:columnGroups 写组标题与组里各列的 key,相邻的列上方共用一个标题。
|
|
53
53
|
* - 合计:列写 summary('sum' 精确相加,或函数)时底部多一行吸底的合计;空值显示列的 empty(默认 —)。
|