@manohub/ui 0.7.0 → 0.8.0
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/README.md +4 -4
- package/dist/components/button/index.d.ts +14 -8
- package/dist/components/capsule/index.d.ts +13 -9
- package/dist/components/capsule/index.js +1 -1
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/card/index.js +7 -4
- package/dist/components/checkbox/index.js +1 -0
- package/dist/components/dialog/index.d.ts +1 -1
- package/dist/components/form/index.d.ts +142 -62
- package/dist/components/form/index.js +41 -29
- package/dist/components/input/index.d.ts +74 -6
- package/dist/components/input/index.js +13 -1
- package/dist/components/layout/index.d.ts +43 -1
- package/dist/components/layout/index.js +22 -2
- package/dist/components/list-view/index.d.ts +1 -1
- package/dist/components/notice/index.d.ts +2 -2
- package/dist/components/notification/index.d.ts +2 -2
- package/dist/components/page/index.js +4 -4
- package/dist/components/pagination/index.d.ts +2 -2
- package/dist/components/pagination/index.js +1 -1
- package/dist/components/panel/index.js +4 -4
- package/dist/components/part-padding.d.ts +9 -6
- package/dist/components/part-padding.js +2 -1
- package/dist/components/query-state/index.d.ts +18 -2
- package/dist/components/radio/index.d.ts +1 -1
- package/dist/components/radio/index.js +1 -0
- package/dist/components/select/index.d.ts +32 -30
- package/dist/components/select/index.js +46 -82
- package/dist/components/select/select-list.d.ts +125 -0
- package/dist/components/select/select-list.js +95 -0
- package/dist/components/select/select-panel.d.ts +106 -0
- package/dist/components/select/select-panel.js +72 -0
- package/dist/components/select/select-shared.d.ts +64 -0
- package/dist/components/select/select-shared.js +24 -0
- package/dist/components/select/select-tree-filter.d.ts +61 -0
- package/dist/components/select/select-tree-filter.js +63 -0
- package/dist/components/select/select-tree.d.ts +274 -0
- package/dist/components/select/select-tree.js +263 -0
- package/dist/components/select/select-trigger.d.ts +132 -0
- package/dist/components/select/select-trigger.js +101 -0
- package/dist/components/steps/index.d.ts +232 -6
- package/dist/components/steps/index.js +84 -26
- package/dist/components/switch/index.d.ts +29 -5
- package/dist/components/switch/index.js +8 -2
- package/dist/components/tabbar/index.d.ts +184 -0
- package/dist/components/tabbar/index.js +161 -0
- package/dist/components/table/index.d.ts +99 -12
- package/dist/components/table/index.js +69 -16
- package/dist/components/tabs/index.d.ts +142 -126
- package/dist/components/tabs/index.js +87 -119
- package/dist/components/tag/index.d.ts +2 -2
- package/dist/components/text/index.d.ts +7 -2
- package/dist/components/toast/index.d.ts +8 -8
- package/dist/components/tree/index.d.ts +28 -2
- package/dist/components/tree/index.js +14 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +6 -3
- package/dist/styles/components/capsule.css +4 -4
- package/dist/styles/components/form.css +198 -39
- package/dist/styles/components/input.css +53 -1
- package/dist/styles/components/layout.css +2 -0
- package/dist/styles/components/select.css +21 -0
- package/dist/styles/components/select.tokens.css +3 -0
- package/dist/styles/components/steps.css +48 -0
- package/dist/styles/components/{tabs.css → tabbar.css} +66 -65
- package/dist/styles/components/tabbar.tokens.css +30 -0
- package/dist/styles/components/table.css +50 -1
- package/dist/styles/components/table.tokens.css +3 -0
- package/dist/styles/index.css +3 -3
- package/dist/styles/operations/tabs.css +73 -0
- package/package.json +2 -2
- package/dist/components/tabset/index.d.ts +0 -109
- package/dist/components/tabset/index.js +0 -75
- package/dist/styles/components/tabs.tokens.css +0 -30
- package/dist/styles/operations/tabset.css +0 -57
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
2
|
-
import type { ControlSize } from '../control-size';
|
|
3
|
-
import { type InputStatus } from '../input';
|
|
4
|
-
export interface SelectOption {
|
|
5
|
-
label: string;
|
|
6
|
-
value: string | number;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
/** 分组标题用的标记(内部判别用,消费方不必写) */
|
|
9
|
-
options?: never;
|
|
10
|
-
}
|
|
11
|
-
/** 分组选项(`{ label: '进行中', options: [...] }`,组内选项与平铺选项同构) */
|
|
12
|
-
export interface SelectOptionGroup {
|
|
13
|
-
label: string;
|
|
14
|
-
options: SelectOption[];
|
|
15
|
-
}
|
|
16
|
-
export type SelectItem = SelectOption | SelectOptionGroup;
|
|
17
|
-
export type SelectValue = string | number | Array<string | number>;
|
|
18
1
|
/**
|
|
19
|
-
* 选择器(**自绘**,不封装 farris
|
|
2
|
+
* Select —— 选择器(**自绘**,不封装 farris);同目录另出 `SelectTree`(树选择)。
|
|
3
|
+
*
|
|
4
|
+
* 本文件是选择器族的**组装层与出口**:`Select` 与 `SelectTree` 都只是把下面这几个内件拼起来,
|
|
5
|
+
* 内件一律不对外导出(`src/index.ts` 只出 `Select` 与 `SelectTree`,成员不平级、也不挂点号)。
|
|
20
6
|
*
|
|
21
|
-
*
|
|
7
|
+
* ```
|
|
8
|
+
* components/select/
|
|
9
|
+
* index.tsx 组装层与出口(Select / SelectTree)
|
|
10
|
+
* select-shared.ts 叶子模块:类型、面板 id、popover 开合、触发器键盘契约
|
|
11
|
+
* select-trigger.tsx 触发器内件(输入框的「触发器形态」)
|
|
12
|
+
* select-panel.tsx 面板壳内件(原生 popover + 搜索行 + 内容槽)
|
|
13
|
+
* select-list.tsx 选项列表体内件(平铺 / 分组 / 创建行 / 空态与加载行)
|
|
14
|
+
* select-tree.tsx 树体内件(SelectTree 的实现)
|
|
15
|
+
* select-tree-filter.ts 纯函数:树过滤与命中切分
|
|
16
|
+
* ```
|
|
22
17
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
18
|
+
* 拆件的收益是**规格只剩一份**:触发器(像输入框 / 清除位 / 箭头 / 键盘开面板)与面板壳
|
|
19
|
+
* (皮肤 / 定位档 / 显式锚 / 搜索行)在「列表形态」与「树形态」之间是同一份代码,改一处两处都跟着走。
|
|
20
|
+
* 面上仍是**一个** `styles/components/select.css`(一件的多种形态共用一份面与一个目录)。
|
|
21
|
+
*
|
|
22
|
+
* ## 触发器的框不在本件
|
|
23
|
+
*
|
|
24
|
+
* 触发器 = 输入框的「触发器形态」(`<Input as="button">`,见 `select-trigger.tsx`):框 / 内距 /
|
|
25
|
+
* 边 / 圆角 / 底 / 字 / hover / 聚焦 / 禁用 / 校验色与**清除位**、**右侧图标位**全部归输入面与它的表壳。
|
|
26
|
+
* 本件只给选择器自己的排版(值 / 标签 / 面板内容)。
|
|
31
27
|
*
|
|
32
28
|
* ## 浮层边界(与「popup 只给零 JS 面」的取舍一致)
|
|
33
29
|
*
|
|
@@ -39,7 +35,7 @@ export type SelectValue = string | number | Array<string | number>;
|
|
|
39
35
|
* ## 状态三源等价
|
|
40
36
|
*
|
|
41
37
|
* 选中行 = `[data-state='selected']`(组件下发)/ 原生 `input:checked`(静态页零 JS 路径)/
|
|
42
|
-
* `.is-selected
|
|
38
|
+
* `.is-selected`(规格冻结档),样式层只写一份声明(见 `select-list.tsx`)。
|
|
43
39
|
*
|
|
44
40
|
* @example 单选
|
|
45
41
|
* <Select options={opts} modelValue={v} placeholder="请选择" onChange={setV} />
|
|
@@ -50,6 +46,12 @@ export type SelectValue = string | number | Array<string | number>;
|
|
|
50
46
|
* @example 分组 + 搜索 + 可创建
|
|
51
47
|
* <Select searchable allowCreate multiple options={grouped} modelValue={list} onChange={setList} />
|
|
52
48
|
*/
|
|
49
|
+
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
50
|
+
import type { ControlSize } from '../control-size';
|
|
51
|
+
import type { InputStatus } from '../input';
|
|
52
|
+
import { type SelectItem, type SelectOption, type SelectValue } from './select-shared';
|
|
53
|
+
export type { SelectItem, SelectOption, SelectOptionGroup, SelectValue } from './select-shared';
|
|
54
|
+
export { SelectTree } from './select-tree';
|
|
53
55
|
export declare const Select: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
54
56
|
/** 选项表(平铺项与分组项可混用) */
|
|
55
57
|
options: {
|
|
@@ -241,12 +243,12 @@ export declare const Select: import("vue").DefineComponent<import("vue").Extract
|
|
|
241
243
|
status: InputStatus;
|
|
242
244
|
clearable: boolean;
|
|
243
245
|
onSearch: (keyword: string) => void;
|
|
246
|
+
emptyText: string;
|
|
247
|
+
searchable: boolean;
|
|
244
248
|
maxTagCount: number;
|
|
245
249
|
maxTagPlaceholder: string;
|
|
246
|
-
searchable: boolean;
|
|
247
250
|
filterOption: (option: SelectOption, keyword: string) => boolean;
|
|
248
251
|
allowCreate: boolean;
|
|
249
|
-
emptyText: string;
|
|
250
252
|
}, SlotsType<{
|
|
251
253
|
empty?: () => VNodeChild;
|
|
252
254
|
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { defineComponent, ref, computed, createVNode, Fragment
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { defineComponent, ref, computed, createVNode, Fragment } from "vue";
|
|
2
|
+
import { SelectList } from "./select-list.js";
|
|
3
|
+
import { SelectPanel } from "./select-panel.js";
|
|
4
|
+
import { SelectTrigger } from "./select-trigger.js";
|
|
5
|
+
import { createSelectPanelId, isGroup, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
|
|
5
6
|
const Select = /* @__PURE__ */ defineComponent({
|
|
6
7
|
name: "Select",
|
|
7
8
|
inheritAttrs: false,
|
|
@@ -102,7 +103,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
102
103
|
slots,
|
|
103
104
|
attrs
|
|
104
105
|
}) {
|
|
105
|
-
const panelId =
|
|
106
|
+
const panelId = createSelectPanelId();
|
|
106
107
|
const panelRef = ref(null);
|
|
107
108
|
const inner = ref(props.defaultValue);
|
|
108
109
|
const keyword = ref("");
|
|
@@ -149,7 +150,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
149
150
|
emit("update:modelValue", value);
|
|
150
151
|
props.onChange?.(value);
|
|
151
152
|
};
|
|
152
|
-
const
|
|
153
|
+
const hide = () => hidePanel(panelRef.value);
|
|
153
154
|
const pick = (option) => {
|
|
154
155
|
if (option.disabled) return;
|
|
155
156
|
if (props.multiple) {
|
|
@@ -158,7 +159,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
158
159
|
return;
|
|
159
160
|
}
|
|
160
161
|
commit(option.value);
|
|
161
|
-
|
|
162
|
+
hide();
|
|
162
163
|
};
|
|
163
164
|
const createFromKeyword = () => {
|
|
164
165
|
const value = keyword.value.trim();
|
|
@@ -175,104 +176,67 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
175
176
|
const next = at < 0 ? step > 0 ? 0 : items.length - 1 : (at + step + items.length) % items.length;
|
|
176
177
|
items[next].focus();
|
|
177
178
|
};
|
|
178
|
-
const onTriggerKeydown = (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
};
|
|
179
|
+
const onTriggerKeydown = createTriggerKeydown({
|
|
180
|
+
clearable: props.clearable,
|
|
181
|
+
disabled: props.disabled,
|
|
182
|
+
clear,
|
|
183
|
+
openPanel: () => showPanel(panelRef.value),
|
|
184
|
+
focusFirst: (direction) => moveFocus(direction)
|
|
185
|
+
});
|
|
188
186
|
const onPanelKeydown = (event) => {
|
|
189
187
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
190
188
|
event.preventDefault();
|
|
191
189
|
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
192
190
|
}
|
|
193
191
|
};
|
|
194
|
-
const renderOption = (option) => createVNode("button", {
|
|
195
|
-
"key": String(option.value),
|
|
196
|
-
"type": "button",
|
|
197
|
-
"class": ["mh-select__option", option.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
198
|
-
"data-state": selectedValues.value.includes(option.value) ? "selected" : "unselected",
|
|
199
|
-
"disabled": option.disabled,
|
|
200
|
-
"role": "option",
|
|
201
|
-
"aria-selected": selectedValues.value.includes(option.value) ? "true" : "false",
|
|
202
|
-
"onClick": () => pick(option)
|
|
203
|
-
}, [option.label]);
|
|
204
192
|
return () => {
|
|
205
193
|
const hasValue = selectedValues.value.length > 0;
|
|
206
194
|
return createVNode("div", {
|
|
207
195
|
"class": ["mh-popup-host", attrs.class].filter(Boolean).join(" "),
|
|
208
196
|
"style": attrs.style
|
|
209
|
-
}, [createVNode(
|
|
210
|
-
"
|
|
197
|
+
}, [createVNode(SelectTrigger, {
|
|
198
|
+
"panelId": panelId,
|
|
211
199
|
"size": props.size,
|
|
212
200
|
"status": props.status,
|
|
213
201
|
"disabled": props.disabled,
|
|
214
202
|
"clearable": props.clearable,
|
|
215
203
|
"hasValue": hasValue,
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
onKeydown: onTriggerKeydown
|
|
223
|
-
},
|
|
224
|
-
"onChange": clear
|
|
225
|
-
}, {
|
|
226
|
-
default: () => props.multiple ? createVNode(Fragment, null, [visibleTags.value.map((option) => createVNode("span", {
|
|
204
|
+
"multiple": props.multiple,
|
|
205
|
+
"placeholder": props.placeholder,
|
|
206
|
+
"label": selectedOptions.value[0]?.label,
|
|
207
|
+
"onKeydown": onTriggerKeydown,
|
|
208
|
+
"onClear": clear,
|
|
209
|
+
"renderValue": props.multiple ? () => createVNode(Fragment, null, [visibleTags.value.map((option) => createVNode("span", {
|
|
227
210
|
"key": String(option.value),
|
|
228
211
|
"class": "mh-select__chip"
|
|
229
212
|
}, [option.label])), foldedCount.value > 0 ? createVNode("span", {
|
|
230
213
|
"class": "mh-select__chip mh-select__chip--fold"
|
|
231
214
|
}, [foldText.value]) : null, !hasValue ? createVNode("span", {
|
|
232
215
|
"class": "mh-select__value mh-select__value--placeholder"
|
|
233
|
-
}, [props.placeholder]) : null]) :
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"id": panelId
|
|
244
|
-
}, {
|
|
245
|
-
popover: "auto"
|
|
246
|
-
}, {
|
|
247
|
-
"class": "mh-popup mh-select__panel mh-popup--anchored",
|
|
248
|
-
"data-side": "bottom",
|
|
249
|
-
"data-align": "start",
|
|
250
|
-
"role": "listbox",
|
|
216
|
+
}, [props.placeholder]) : null]) : void 0
|
|
217
|
+
}, null), createVNode(SelectPanel, {
|
|
218
|
+
"id": panelId,
|
|
219
|
+
"elementRef": panelRef,
|
|
220
|
+
"searchable": showSearch.value,
|
|
221
|
+
"keyword": keyword.value,
|
|
222
|
+
"onKeyword": (value) => {
|
|
223
|
+
keyword.value = value;
|
|
224
|
+
props.onSearch?.(value);
|
|
225
|
+
},
|
|
251
226
|
"onKeydown": onPanelKeydown
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
keyword
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
"class": "mh-select__option mh-select__option--empty"
|
|
266
|
-
}, [props.emptyText]) : createVNode(Fragment, null, [visibleOptions.value.map((item, index) => isGroup(item) ? createVNode("div", {
|
|
267
|
-
"key": `group-${index}`,
|
|
268
|
-
"class": "mh-select__group"
|
|
269
|
-
}, [createVNode("span", {
|
|
270
|
-
"class": "mh-select__group-label"
|
|
271
|
-
}, [item.label]), item.options.map((option) => renderOption(option))]) : renderOption(item)), creatable.value ? createVNode("button", {
|
|
272
|
-
"type": "button",
|
|
273
|
-
"class": "mh-select__option mh-select__add",
|
|
274
|
-
"onClick": createFromKeyword
|
|
275
|
-
}, [createTextVNode("创建「"), keyword.value.trim(), createTextVNode("」")]) : null])])]);
|
|
227
|
+
}, {
|
|
228
|
+
default: () => createVNode(SelectList, {
|
|
229
|
+
"options": visibleOptions.value,
|
|
230
|
+
"selectedValues": selectedValues.value,
|
|
231
|
+
"loading": props.loading,
|
|
232
|
+
"emptyText": props.emptyText,
|
|
233
|
+
"creatable": creatable.value,
|
|
234
|
+
"keyword": keyword.value,
|
|
235
|
+
"onPick": pick,
|
|
236
|
+
"onCreate": createFromKeyword,
|
|
237
|
+
"renderEmpty": slots.empty
|
|
238
|
+
}, null)
|
|
239
|
+
})]);
|
|
276
240
|
};
|
|
277
241
|
}
|
|
278
242
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectList —— 选择器族的**选项列表体内件**(`Select` 用,不对外导出)。
|
|
3
|
+
*
|
|
4
|
+
* 它拥有「面板里那一撮选项」的全部形态与状态行:平铺选项 / 分组(组标题不可点)/ 「创建」行 /
|
|
5
|
+
* 空态行 / 加载行。**不含面板壳与搜索行**(那是 `SelectPanel` 的事),也不持有任何状态 ——
|
|
6
|
+
* 过滤结果、选中值、是否可创建统统由消费方算好喂进来,本件只渲染。
|
|
7
|
+
*
|
|
8
|
+
* 选项是真实 `<button role="option">`(不是 div + JS 高亮):键盘可达、`:focus-visible` 可直接
|
|
9
|
+
* 承载「高亮行」这层语义,于是不需要再维护一份 JS 高亮下标 —— **焦点即高亮**,
|
|
10
|
+
* 状态只有 DOM 焦点一个来源。四条按钮重置(`border` / `background` / `font-family` /
|
|
11
|
+
* `text-align`)在面上自带(reset 层清了前三条、`text-align` 属列表行语义故面里显式写),
|
|
12
|
+
* 否则选项会长成原生按钮的灰底立体框 —— 见 `select.css` 的 `.mh-select__option` 一节。
|
|
13
|
+
*
|
|
14
|
+
* 选中态**三源等价**(面层只写一份声明):组件下发 `[data-state='selected']` /
|
|
15
|
+
* 原生 `:has(input:checked)`(静态页零 JS 单选路径)/ `.is-selected`(规格冻结档)。
|
|
16
|
+
*/
|
|
17
|
+
import { type PropType, type VNodeChild } from 'vue';
|
|
18
|
+
import { type SelectItem, type SelectOption } from './select-shared';
|
|
19
|
+
export declare const SelectList: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
20
|
+
/** 已经过滤过的渲染树(分组内全空则由消费方整组去掉) */
|
|
21
|
+
options: {
|
|
22
|
+
type: PropType<SelectItem[]>;
|
|
23
|
+
default: () => never[];
|
|
24
|
+
};
|
|
25
|
+
/** 当前选中值(多选为数组;只用于判定选项行的选中态) */
|
|
26
|
+
selectedValues: {
|
|
27
|
+
type: PropType<Array<string | number>>;
|
|
28
|
+
default: () => never[];
|
|
29
|
+
};
|
|
30
|
+
loading: {
|
|
31
|
+
type: BooleanConstructor;
|
|
32
|
+
default: boolean;
|
|
33
|
+
};
|
|
34
|
+
emptyText: {
|
|
35
|
+
type: StringConstructor;
|
|
36
|
+
default: string;
|
|
37
|
+
};
|
|
38
|
+
/** 可创建:搜索词无命中时由消费方判定,本件只负责渲染那一行 */
|
|
39
|
+
creatable: {
|
|
40
|
+
type: BooleanConstructor;
|
|
41
|
+
default: boolean;
|
|
42
|
+
};
|
|
43
|
+
/** 当前搜索词(「创建「xxx」」那一行的文案来源) */
|
|
44
|
+
keyword: {
|
|
45
|
+
type: StringConstructor;
|
|
46
|
+
default: string;
|
|
47
|
+
};
|
|
48
|
+
onPick: {
|
|
49
|
+
type: PropType<(option: SelectOption) => void>;
|
|
50
|
+
default: undefined;
|
|
51
|
+
};
|
|
52
|
+
onCreate: {
|
|
53
|
+
type: PropType<() => void>;
|
|
54
|
+
default: undefined;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* 空态自定义内容。
|
|
58
|
+
*
|
|
59
|
+
* 用**函数 prop** 而不是插槽:内件不对外,插槽还要多一层 `SlotsType` 签名与父层的对象写法,
|
|
60
|
+
* 而这一处只有「给或是不给」两种。返回 `undefined` 即走内建的空态行。
|
|
61
|
+
*/
|
|
62
|
+
renderEmpty: {
|
|
63
|
+
type: PropType<() => VNodeChild>;
|
|
64
|
+
default: undefined;
|
|
65
|
+
};
|
|
66
|
+
}>, () => string | number | boolean | import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
}> | import("vue").VNodeArrayChildren, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
69
|
+
/** 已经过滤过的渲染树(分组内全空则由消费方整组去掉) */
|
|
70
|
+
options: {
|
|
71
|
+
type: PropType<SelectItem[]>;
|
|
72
|
+
default: () => never[];
|
|
73
|
+
};
|
|
74
|
+
/** 当前选中值(多选为数组;只用于判定选项行的选中态) */
|
|
75
|
+
selectedValues: {
|
|
76
|
+
type: PropType<Array<string | number>>;
|
|
77
|
+
default: () => never[];
|
|
78
|
+
};
|
|
79
|
+
loading: {
|
|
80
|
+
type: BooleanConstructor;
|
|
81
|
+
default: boolean;
|
|
82
|
+
};
|
|
83
|
+
emptyText: {
|
|
84
|
+
type: StringConstructor;
|
|
85
|
+
default: string;
|
|
86
|
+
};
|
|
87
|
+
/** 可创建:搜索词无命中时由消费方判定,本件只负责渲染那一行 */
|
|
88
|
+
creatable: {
|
|
89
|
+
type: BooleanConstructor;
|
|
90
|
+
default: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** 当前搜索词(「创建「xxx」」那一行的文案来源) */
|
|
93
|
+
keyword: {
|
|
94
|
+
type: StringConstructor;
|
|
95
|
+
default: string;
|
|
96
|
+
};
|
|
97
|
+
onPick: {
|
|
98
|
+
type: PropType<(option: SelectOption) => void>;
|
|
99
|
+
default: undefined;
|
|
100
|
+
};
|
|
101
|
+
onCreate: {
|
|
102
|
+
type: PropType<() => void>;
|
|
103
|
+
default: undefined;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* 空态自定义内容。
|
|
107
|
+
*
|
|
108
|
+
* 用**函数 prop** 而不是插槽:内件不对外,插槽还要多一层 `SlotsType` 签名与父层的对象写法,
|
|
109
|
+
* 而这一处只有「给或是不给」两种。返回 `undefined` 即走内建的空态行。
|
|
110
|
+
*/
|
|
111
|
+
renderEmpty: {
|
|
112
|
+
type: PropType<() => VNodeChild>;
|
|
113
|
+
default: undefined;
|
|
114
|
+
};
|
|
115
|
+
}>> & Readonly<{}>, {
|
|
116
|
+
loading: boolean;
|
|
117
|
+
options: SelectItem[];
|
|
118
|
+
selectedValues: (string | number)[];
|
|
119
|
+
emptyText: string;
|
|
120
|
+
creatable: boolean;
|
|
121
|
+
keyword: string;
|
|
122
|
+
onPick: (option: SelectOption) => void;
|
|
123
|
+
onCreate: () => void;
|
|
124
|
+
renderEmpty: () => VNodeChild;
|
|
125
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineComponent, createVNode, createTextVNode, Fragment } from "vue";
|
|
2
|
+
import { isGroup } from "./select-shared.js";
|
|
3
|
+
const SelectList = /* @__PURE__ */ defineComponent({
|
|
4
|
+
name: "SelectList",
|
|
5
|
+
inheritAttrs: false,
|
|
6
|
+
props: {
|
|
7
|
+
/** 已经过滤过的渲染树(分组内全空则由消费方整组去掉) */
|
|
8
|
+
options: {
|
|
9
|
+
type: Array,
|
|
10
|
+
default: () => []
|
|
11
|
+
},
|
|
12
|
+
/** 当前选中值(多选为数组;只用于判定选项行的选中态) */
|
|
13
|
+
selectedValues: {
|
|
14
|
+
type: Array,
|
|
15
|
+
default: () => []
|
|
16
|
+
},
|
|
17
|
+
loading: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false
|
|
20
|
+
},
|
|
21
|
+
emptyText: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: "暂无数据"
|
|
24
|
+
},
|
|
25
|
+
/** 可创建:搜索词无命中时由消费方判定,本件只负责渲染那一行 */
|
|
26
|
+
creatable: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: false
|
|
29
|
+
},
|
|
30
|
+
/** 当前搜索词(「创建「xxx」」那一行的文案来源) */
|
|
31
|
+
keyword: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: ""
|
|
34
|
+
},
|
|
35
|
+
onPick: {
|
|
36
|
+
type: Function,
|
|
37
|
+
default: void 0
|
|
38
|
+
},
|
|
39
|
+
onCreate: {
|
|
40
|
+
type: Function,
|
|
41
|
+
default: void 0
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* 空态自定义内容。
|
|
45
|
+
*
|
|
46
|
+
* 用**函数 prop** 而不是插槽:内件不对外,插槽还要多一层 `SlotsType` 签名与父层的对象写法,
|
|
47
|
+
* 而这一处只有「给或是不给」两种。返回 `undefined` 即走内建的空态行。
|
|
48
|
+
*/
|
|
49
|
+
renderEmpty: {
|
|
50
|
+
type: Function,
|
|
51
|
+
default: void 0
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
setup(props) {
|
|
55
|
+
const renderOption = (option) => {
|
|
56
|
+
const selected = props.selectedValues.includes(option.value);
|
|
57
|
+
return createVNode("button", {
|
|
58
|
+
"key": String(option.value),
|
|
59
|
+
"type": "button",
|
|
60
|
+
"class": ["mh-select__option", option.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
61
|
+
"data-state": selected ? "selected" : "unselected",
|
|
62
|
+
"disabled": option.disabled,
|
|
63
|
+
"role": "option",
|
|
64
|
+
"aria-selected": selected ? "true" : "false",
|
|
65
|
+
"onClick": () => props.onPick?.(option)
|
|
66
|
+
}, [option.label]);
|
|
67
|
+
};
|
|
68
|
+
return () => {
|
|
69
|
+
if (props.loading) {
|
|
70
|
+
return createVNode("span", {
|
|
71
|
+
"class": "mh-select__option mh-select__option--hint"
|
|
72
|
+
}, [createTextVNode("加载中…")]);
|
|
73
|
+
}
|
|
74
|
+
if (props.options.length === 0 && !props.creatable) {
|
|
75
|
+
const custom = props.renderEmpty?.();
|
|
76
|
+
return custom !== void 0 && custom !== null ? custom : createVNode("span", {
|
|
77
|
+
"class": "mh-select__option mh-select__option--empty"
|
|
78
|
+
}, [props.emptyText]);
|
|
79
|
+
}
|
|
80
|
+
return createVNode(Fragment, null, [props.options.map((item, index) => isGroup(item) ? createVNode("div", {
|
|
81
|
+
"key": `group-${index}`,
|
|
82
|
+
"class": "mh-select__group"
|
|
83
|
+
}, [createVNode("span", {
|
|
84
|
+
"class": "mh-select__group-label"
|
|
85
|
+
}, [item.label]), item.options.map((option) => renderOption(option))]) : renderOption(item)), props.creatable ? createVNode("button", {
|
|
86
|
+
"type": "button",
|
|
87
|
+
"class": "mh-select__option mh-select__add",
|
|
88
|
+
"onClick": () => props.onCreate?.()
|
|
89
|
+
}, [createTextVNode("创建「"), props.keyword.trim(), createTextVNode("」")]) : null]);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
export {
|
|
94
|
+
SelectList
|
|
95
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectPanel —— 选择器族的**面板壳内件**(`Select` 与 `SelectTree` 共用,不对外导出)。
|
|
3
|
+
*
|
|
4
|
+
* 它只拥有三样东西:**原生 popover 元素**(开合 / Esc / 点外部关闭 / 顶层渲染 / 定位全归浏览器)、
|
|
5
|
+
* **搜索行**(面板顶部那一个输入框)、以及**内容槽**(列表体或树体)。
|
|
6
|
+
* 「面板里装什么」不在本件 —— 这正是让列表与树共用同一份面板皮肤的原因。
|
|
7
|
+
*
|
|
8
|
+
* 组合类名与服务端三条属性都是**操作层的组合**,不是本件新造的浮层:
|
|
9
|
+
* `class="mh-popup mh-select__panel mh-popup--anchored"`(状态根 + 本族皮肤 + 显式锚档)、
|
|
10
|
+
* `data-side` / `data-align`(`popup.css` 的定位档)、`popover="auto"`(原生轻 dismiss)。
|
|
11
|
+
* 面板宽度跟触发器走:`select.css` 的 `.mh-select__panel` 用 `anchor-size(width)` 取锚点宽,
|
|
12
|
+
* 锚是挂在**表壳**上的 `.mh-popup-anchor`(触发器进壳后隐式锚会缩成壳内控件,故走显式锚档)。
|
|
13
|
+
*
|
|
14
|
+
* `elementRef` 是**由消费方持有的 ref**(不是本件自己的):开面板(触发器 ↑↓)、收起面板(选中后)、
|
|
15
|
+
* 以及列表形态的 `.mh-select__option` 焦点遍历都要在父层拿到这个 DOM 元素。
|
|
16
|
+
*/
|
|
17
|
+
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
18
|
+
import type { ElementRef } from './select-shared';
|
|
19
|
+
export declare const SelectPanel: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
20
|
+
/** 面板 id(必须与触发器的 `popovertarget` 成对) */
|
|
21
|
+
id: {
|
|
22
|
+
type: StringConstructor;
|
|
23
|
+
required: true;
|
|
24
|
+
};
|
|
25
|
+
/** 面板元素的 ref,由消费方持有(父层要用它开合与遍历选项) */
|
|
26
|
+
elementRef: {
|
|
27
|
+
type: PropType<ElementRef>;
|
|
28
|
+
default: undefined;
|
|
29
|
+
};
|
|
30
|
+
/** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
|
|
31
|
+
searchable: {
|
|
32
|
+
type: BooleanConstructor;
|
|
33
|
+
default: boolean;
|
|
34
|
+
};
|
|
35
|
+
keyword: {
|
|
36
|
+
type: StringConstructor;
|
|
37
|
+
default: string;
|
|
38
|
+
};
|
|
39
|
+
searchPlaceholder: {
|
|
40
|
+
type: StringConstructor;
|
|
41
|
+
default: string;
|
|
42
|
+
};
|
|
43
|
+
/** 搜索词变化(消费方负责写回 keyword,并自行决定要不要上抛 `onSearch`) */
|
|
44
|
+
onKeyword: {
|
|
45
|
+
type: PropType<(keyword: string) => void>;
|
|
46
|
+
default: undefined;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* 面板上的键盘契约。
|
|
50
|
+
*
|
|
51
|
+
* 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
|
|
52
|
+
* 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
|
|
53
|
+
*/
|
|
54
|
+
onKeydown: {
|
|
55
|
+
type: PropType<(event: KeyboardEvent) => void>;
|
|
56
|
+
default: undefined;
|
|
57
|
+
};
|
|
58
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
59
|
+
/** 面板 id(必须与触发器的 `popovertarget` 成对) */
|
|
60
|
+
id: {
|
|
61
|
+
type: StringConstructor;
|
|
62
|
+
required: true;
|
|
63
|
+
};
|
|
64
|
+
/** 面板元素的 ref,由消费方持有(父层要用它开合与遍历选项) */
|
|
65
|
+
elementRef: {
|
|
66
|
+
type: PropType<ElementRef>;
|
|
67
|
+
default: undefined;
|
|
68
|
+
};
|
|
69
|
+
/** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
|
|
70
|
+
searchable: {
|
|
71
|
+
type: BooleanConstructor;
|
|
72
|
+
default: boolean;
|
|
73
|
+
};
|
|
74
|
+
keyword: {
|
|
75
|
+
type: StringConstructor;
|
|
76
|
+
default: string;
|
|
77
|
+
};
|
|
78
|
+
searchPlaceholder: {
|
|
79
|
+
type: StringConstructor;
|
|
80
|
+
default: string;
|
|
81
|
+
};
|
|
82
|
+
/** 搜索词变化(消费方负责写回 keyword,并自行决定要不要上抛 `onSearch`) */
|
|
83
|
+
onKeyword: {
|
|
84
|
+
type: PropType<(keyword: string) => void>;
|
|
85
|
+
default: undefined;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* 面板上的键盘契约。
|
|
89
|
+
*
|
|
90
|
+
* 列表形态给 `↑↓` 移动高亮行(选项是真实 `<button>`,焦点即高亮);树形态不给 ——
|
|
91
|
+
* 树的行是 `div`、没有 tabindex,焦点无处可落(与 `Tree` 自身不做键盘导航同口径)。
|
|
92
|
+
*/
|
|
93
|
+
onKeydown: {
|
|
94
|
+
type: PropType<(event: KeyboardEvent) => void>;
|
|
95
|
+
default: undefined;
|
|
96
|
+
};
|
|
97
|
+
}>> & Readonly<{}>, {
|
|
98
|
+
onKeydown: (event: KeyboardEvent) => void;
|
|
99
|
+
keyword: string;
|
|
100
|
+
elementRef: ElementRef;
|
|
101
|
+
searchable: boolean;
|
|
102
|
+
searchPlaceholder: string;
|
|
103
|
+
onKeyword: (keyword: string) => void;
|
|
104
|
+
}, SlotsType<{
|
|
105
|
+
default?: () => VNodeChild;
|
|
106
|
+
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|