@manohub/ui 0.7.1 → 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 +1 -1
- package/dist/components/button/index.d.ts +6 -0
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/form/index.d.ts +28 -46
- package/dist/components/form/index.js +3 -21
- package/dist/components/input/index.d.ts +74 -6
- package/dist/components/input/index.js +13 -1
- package/dist/components/list-view/index.d.ts +1 -1
- package/dist/components/pagination/index.d.ts +2 -2
- package/dist/components/pagination/index.js +1 -1
- package/dist/components/query-state/index.d.ts +18 -2
- 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 +1 -1
- package/dist/components/table/index.d.ts +25 -4
- package/dist/components/text/index.d.ts +7 -2
- package/dist/components/tree/index.d.ts +28 -2
- package/dist/components/tree/index.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/styles/components/form.css +0 -21
- package/dist/styles/components/input.css +25 -0
- package/dist/styles/components/select.css +21 -0
- package/dist/styles/components/select.tokens.css +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 选择器族 · 树形态的**纯函数层**(过滤、命中切分、按值找节点)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一层:这三件事都是**数据变换**,与渲染无关,却最容易写错(祖先链要不要留、
|
|
5
|
+
* 父命中要不要连整支、多次命中怎么切)。抽成不依赖 DOM 的函数后可以直接单测 ——
|
|
6
|
+
* 用例是 `__tests__/select-tree-filter.spec.ts`,不必挂组件、也不必造 DOM。
|
|
7
|
+
*
|
|
8
|
+
* 本文件不 import 任何 Vue 运行时代码(只有类型),也不产出类名 ——
|
|
9
|
+
* 它是 `select/` 目录里的叶子模块之一。
|
|
10
|
+
*/
|
|
11
|
+
import type { TreeNode } from '../tree';
|
|
12
|
+
/** 树的三条取值口径(与 `Tree` 的同名 prop 同义,故整包传同一个对象即可) */
|
|
13
|
+
export interface TreeAccessors {
|
|
14
|
+
/** 节点主键:字段名或取值函数 */
|
|
15
|
+
rowKey: string | ((node: TreeNode) => string);
|
|
16
|
+
/** 子节点字段名 */
|
|
17
|
+
childrenKey: string;
|
|
18
|
+
/** 标签字段名 */
|
|
19
|
+
labelKey: string;
|
|
20
|
+
}
|
|
21
|
+
/** 取节点主键(与 `Tree` 内部的 `String()` 口径一致:值一律以字符串参与比较) */
|
|
22
|
+
export declare const treeNodeKey: (node: TreeNode, rowKey: TreeAccessors["rowKey"]) => string;
|
|
23
|
+
/** 取子节点(字段不是数组一律当没有子节点,与 `Tree` 同款宽容) */
|
|
24
|
+
export declare const treeChildren: (node: TreeNode, childrenKey: string) => TreeNode[];
|
|
25
|
+
/** 取标签文本 */
|
|
26
|
+
export declare const treeLabel: (node: TreeNode, labelKey: string) => string;
|
|
27
|
+
/** 按主键在整棵树里找节点(触发器要显示选中节点的名称;找不到时由消费方回退显示原值) */
|
|
28
|
+
export declare function findTreeNode(nodes: TreeNode[], key: string, accessors: Pick<TreeAccessors, 'rowKey' | 'childrenKey'>): TreeNode | undefined;
|
|
29
|
+
export interface TreeFilterOptions extends TreeAccessors {
|
|
30
|
+
/** 关键词(调用方保证非空;空串走「不过滤」那条路,不进本函数) */
|
|
31
|
+
keyword: string;
|
|
32
|
+
/** 自定义命中判定(缺省:标签**区分大小写**地包含关键词 —— 与 `Select` 的缺省过滤同口径) */
|
|
33
|
+
match?: (node: TreeNode, keyword: string) => boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface TreeFilterResult {
|
|
36
|
+
/** 过滤后的树(命中项 + 它们的祖先链;被剪掉的分支不在其中) */
|
|
37
|
+
nodes: TreeNode[];
|
|
38
|
+
/** 结果里出现的**全部**节点 key(含祖先)—— 消费方用它做「过滤态强制全展」 */
|
|
39
|
+
keys: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 过滤树。三条规则(**父命中即整支可见**,与「搜到的域要连带其下所有条目」的直觉一致):
|
|
43
|
+
*
|
|
44
|
+
* 1. 节点**自身命中** → 保留它**及其整棵子树**(子树原引用,不改数据);
|
|
45
|
+
* 2. 自身未命中但**后代有命中** → 保留该节点 + 递归过滤后的子树(即**保留祖先链**);
|
|
46
|
+
* 3. 整支无命中 → 剪掉。
|
|
47
|
+
*
|
|
48
|
+
* 复杂度 O(N)(每个节点只看一次),只在关键词或数据变化时由 `computed` 触发。
|
|
49
|
+
*/
|
|
50
|
+
export declare function filterTreeNodes(nodes: TreeNode[], options: TreeFilterOptions): TreeFilterResult;
|
|
51
|
+
/** 命中切分的一段:`hit` 为真时由面层给底色(`.mh-select__hit`) */
|
|
52
|
+
export interface HitSegment {
|
|
53
|
+
text: string;
|
|
54
|
+
hit: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 把标签文本按关键词切成「命中 / 未命中」交替的片段(**全部**出现位置都切,不只第一处)。
|
|
58
|
+
*
|
|
59
|
+
* 关键词为空、或压根没命中时返回**一整段未命中**(消费方据此仍能渲染出原文,不必分情况)。
|
|
60
|
+
*/
|
|
61
|
+
export declare function splitHighlight(text: string, keyword: string): HitSegment[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const treeNodeKey = (node, rowKey) => typeof rowKey === "function" ? rowKey(node) : String(node[rowKey] ?? "");
|
|
2
|
+
const treeChildren = (node, childrenKey) => {
|
|
3
|
+
const children = node[childrenKey];
|
|
4
|
+
return Array.isArray(children) ? children : [];
|
|
5
|
+
};
|
|
6
|
+
const treeLabel = (node, labelKey) => String(node[labelKey] ?? "");
|
|
7
|
+
function findTreeNode(nodes, key, accessors) {
|
|
8
|
+
for (const node of nodes) {
|
|
9
|
+
if (treeNodeKey(node, accessors.rowKey) === key) return node;
|
|
10
|
+
const found = findTreeNode(treeChildren(node, accessors.childrenKey), key, accessors);
|
|
11
|
+
if (found) return found;
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
function filterTreeNodes(nodes, options) {
|
|
16
|
+
const hit = (node) => options.match ? options.match(node, options.keyword) : treeLabel(node, options.labelKey).includes(options.keyword);
|
|
17
|
+
const keys = [];
|
|
18
|
+
const collectKeys = (node) => {
|
|
19
|
+
keys.push(treeNodeKey(node, options.rowKey));
|
|
20
|
+
for (const child of treeChildren(node, options.childrenKey)) collectKeys(child);
|
|
21
|
+
};
|
|
22
|
+
const walk = (list) => {
|
|
23
|
+
const kept = [];
|
|
24
|
+
for (const node of list) {
|
|
25
|
+
if (hit(node)) {
|
|
26
|
+
collectKeys(node);
|
|
27
|
+
kept.push(node);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
keys.push(treeNodeKey(node, options.rowKey));
|
|
31
|
+
const keptChildren = walk(treeChildren(node, options.childrenKey));
|
|
32
|
+
if (keptChildren.length) {
|
|
33
|
+
kept.push({ ...node, [options.childrenKey]: keptChildren });
|
|
34
|
+
} else {
|
|
35
|
+
keys.pop();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return kept;
|
|
39
|
+
};
|
|
40
|
+
return { nodes: walk(nodes), keys };
|
|
41
|
+
}
|
|
42
|
+
function splitHighlight(text, keyword) {
|
|
43
|
+
if (!keyword) return [{ text, hit: false }];
|
|
44
|
+
const segments = [];
|
|
45
|
+
let cursor = 0;
|
|
46
|
+
for (; ; ) {
|
|
47
|
+
const at = text.indexOf(keyword, cursor);
|
|
48
|
+
if (at < 0) break;
|
|
49
|
+
if (at > cursor) segments.push({ text: text.slice(cursor, at), hit: false });
|
|
50
|
+
segments.push({ text: text.slice(at, at + keyword.length), hit: true });
|
|
51
|
+
cursor = at + keyword.length;
|
|
52
|
+
}
|
|
53
|
+
if (cursor < text.length) segments.push({ text: text.slice(cursor), hit: false });
|
|
54
|
+
return segments.length ? segments : [{ text, hit: false }];
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
filterTreeNodes,
|
|
58
|
+
findTreeNode,
|
|
59
|
+
splitHighlight,
|
|
60
|
+
treeChildren,
|
|
61
|
+
treeLabel,
|
|
62
|
+
treeNodeKey
|
|
63
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectTree —— **树选择**(`Select` 的树形态,与它平级导出:`import { SelectTree } from '@manohub/ui'`)。
|
|
3
|
+
*
|
|
4
|
+
* 它与 `Select` 共用**同一批内件**:触发器(像输入框 / 清除位 / 箭头 / 键盘开面板)、
|
|
5
|
+
* 面板壳(皮肤 / 定位档 / 显式锚 / 搜索行)、共用原子(面板 id / popover 开合 / 键盘契约)。
|
|
6
|
+
* 所以「面板长相与开合行为」两件只有一份规格;本件独有的只有**树体那一层**:
|
|
7
|
+
*
|
|
8
|
+
* 1. **仅叶子可选**:点叶子行即选中并收起面板;点父节点行只切换展开(不产生选中值)——
|
|
9
|
+
* 父节点是「分组」而不是「选项」,选中它语义上是空的(`Tree` 自身不做这个判断,故在此兜住)。
|
|
10
|
+
* 2. **本地过滤树**:搜索行在场时按关键词过滤,命中节点的**祖先链保留**、父节点自身命中则
|
|
11
|
+
* **整支保留**,并强制全展到命中层(算法与切分都是纯函数,见 `select-tree-filter.ts`);
|
|
12
|
+
* 命中文本按面层的 `.mh-select__hit` 高亮(靠 `Tree` 的可选 `renderLabel` 钩子落进标签槽)。
|
|
13
|
+
* 3. **面板内限高滚动**:树体包在 `.mh-select__tree` 里(内距 + 限高 + 溢出滚动),长树不撑破面板。
|
|
14
|
+
*
|
|
15
|
+
* ## 与 `Select` 的三处有意差异(都是「树不是列表」决定的)
|
|
16
|
+
*
|
|
17
|
+
* | 事项 | `Select` | `SelectTree` |
|
|
18
|
+
* | --- | --- | --- |
|
|
19
|
+
* | 值 | `string \| number \| Array<…>` | 只单选、只 `string`(`Tree` 的主键本就 `String()` 化) |
|
|
20
|
+
* | 面板内 ↑↓ 行导航 | 有(选项是真实 `<button>`,焦点即高亮) | **无**(树的行是 `div`、没有 tabindex;`Tree` 自身也不做键盘导航) |
|
|
21
|
+
* | 空 / 加载态 | 列表体的内建行 | 面板内同样出一行文字(**不复用**整件三态容器:下拉里出空态插画太重) |
|
|
22
|
+
*
|
|
23
|
+
* @example 基础单选
|
|
24
|
+
* <SelectTree nodes={tree} rowKey="id" modelValue={v} onChange={setV} placeholder="请选择业务域" />
|
|
25
|
+
*
|
|
26
|
+
* @example 搜索 + 默认全展开
|
|
27
|
+
* <SelectTree nodes={tree} rowKey="id" labelKey="name" searchable defaultExpandAll onChange={setV} />
|
|
28
|
+
*/
|
|
29
|
+
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
30
|
+
import type { ControlSize } from '../control-size';
|
|
31
|
+
import type { InputStatus } from '../input';
|
|
32
|
+
import { type TreeNode } from '../tree';
|
|
33
|
+
export declare const SelectTree: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
34
|
+
/** 树数据(与 `Tree` 同一个 `TreeNode` 类型,不另造节点形状) */
|
|
35
|
+
nodes: {
|
|
36
|
+
type: PropType<TreeNode[]>;
|
|
37
|
+
required: true;
|
|
38
|
+
};
|
|
39
|
+
/** 节点主键:字段名或取值函数(与 `Tree` 同义) */
|
|
40
|
+
rowKey: {
|
|
41
|
+
type: PropType<string | ((node: TreeNode) => string)>;
|
|
42
|
+
required: true;
|
|
43
|
+
};
|
|
44
|
+
/** 标签字段名(默认 'label') */
|
|
45
|
+
labelKey: {
|
|
46
|
+
type: StringConstructor;
|
|
47
|
+
default: string;
|
|
48
|
+
};
|
|
49
|
+
/** 子节点字段名(默认 'children') */
|
|
50
|
+
childrenKey: {
|
|
51
|
+
type: StringConstructor;
|
|
52
|
+
default: string;
|
|
53
|
+
};
|
|
54
|
+
/** 受控值(选中叶子的主键;树选择只单选) */
|
|
55
|
+
modelValue: {
|
|
56
|
+
type: StringConstructor;
|
|
57
|
+
default: undefined;
|
|
58
|
+
};
|
|
59
|
+
/** 非受控初始值 */
|
|
60
|
+
defaultValue: {
|
|
61
|
+
type: StringConstructor;
|
|
62
|
+
default: undefined;
|
|
63
|
+
};
|
|
64
|
+
placeholder: {
|
|
65
|
+
type: StringConstructor;
|
|
66
|
+
default: string;
|
|
67
|
+
};
|
|
68
|
+
/** 搜索:`true` 强制开;不给时节点总数多于 8 自动开(与 `Select` 同口径) */
|
|
69
|
+
searchable: {
|
|
70
|
+
type: BooleanConstructor;
|
|
71
|
+
default: boolean;
|
|
72
|
+
};
|
|
73
|
+
/** 自定义命中判定(缺省:标签区分大小写地包含关键词) */
|
|
74
|
+
filterNode: {
|
|
75
|
+
type: PropType<(node: TreeNode, keyword: string) => boolean>;
|
|
76
|
+
default: undefined;
|
|
77
|
+
};
|
|
78
|
+
/** 命中文本是否高亮(默认开) */
|
|
79
|
+
highlight: {
|
|
80
|
+
type: BooleanConstructor;
|
|
81
|
+
default: boolean;
|
|
82
|
+
};
|
|
83
|
+
/** 受控展开集(传了就完全受控;**过滤态下组件强制全展**,不受本 prop 约束) */
|
|
84
|
+
expandedKeys: {
|
|
85
|
+
type: PropType<string[]>;
|
|
86
|
+
default: undefined;
|
|
87
|
+
};
|
|
88
|
+
/** 非受控展开初值 */
|
|
89
|
+
defaultExpandedKeys: {
|
|
90
|
+
type: PropType<string[]>;
|
|
91
|
+
default: () => never[];
|
|
92
|
+
};
|
|
93
|
+
/** 初始展开全部(与 `defaultExpandedKeys` 互斥,它优先) */
|
|
94
|
+
defaultExpandAll: {
|
|
95
|
+
type: BooleanConstructor;
|
|
96
|
+
default: boolean;
|
|
97
|
+
};
|
|
98
|
+
/** 展开集变化回调 */
|
|
99
|
+
onExpandChange: {
|
|
100
|
+
type: PropType<(keys: string[]) => void>;
|
|
101
|
+
default: undefined;
|
|
102
|
+
};
|
|
103
|
+
clearable: {
|
|
104
|
+
type: BooleanConstructor;
|
|
105
|
+
default: boolean;
|
|
106
|
+
};
|
|
107
|
+
disabled: {
|
|
108
|
+
type: BooleanConstructor;
|
|
109
|
+
default: boolean;
|
|
110
|
+
};
|
|
111
|
+
/** 加载中(面板里出一行提示,不渲染树) */
|
|
112
|
+
loading: {
|
|
113
|
+
type: BooleanConstructor;
|
|
114
|
+
default: boolean;
|
|
115
|
+
};
|
|
116
|
+
emptyText: {
|
|
117
|
+
type: StringConstructor;
|
|
118
|
+
default: string;
|
|
119
|
+
};
|
|
120
|
+
/** 尺寸档(24 / 28 / 32) */
|
|
121
|
+
size: {
|
|
122
|
+
type: PropType<ControlSize>;
|
|
123
|
+
default: string;
|
|
124
|
+
};
|
|
125
|
+
/** 校验状态(红 / 黄;与 `Input` 同一词表) */
|
|
126
|
+
status: {
|
|
127
|
+
type: PropType<InputStatus>;
|
|
128
|
+
default: undefined;
|
|
129
|
+
};
|
|
130
|
+
/** 值变更回调(选中叶子的主键) */
|
|
131
|
+
onChange: {
|
|
132
|
+
type: PropType<(v: string) => void>;
|
|
133
|
+
default: undefined;
|
|
134
|
+
};
|
|
135
|
+
/** 搜索词变更(远程换数据用;本地过滤不受它影响) */
|
|
136
|
+
onSearch: {
|
|
137
|
+
type: PropType<(keyword: string) => void>;
|
|
138
|
+
default: undefined;
|
|
139
|
+
};
|
|
140
|
+
}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
141
|
+
'update:modelValue': (v: string) => true;
|
|
142
|
+
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
143
|
+
/** 树数据(与 `Tree` 同一个 `TreeNode` 类型,不另造节点形状) */
|
|
144
|
+
nodes: {
|
|
145
|
+
type: PropType<TreeNode[]>;
|
|
146
|
+
required: true;
|
|
147
|
+
};
|
|
148
|
+
/** 节点主键:字段名或取值函数(与 `Tree` 同义) */
|
|
149
|
+
rowKey: {
|
|
150
|
+
type: PropType<string | ((node: TreeNode) => string)>;
|
|
151
|
+
required: true;
|
|
152
|
+
};
|
|
153
|
+
/** 标签字段名(默认 'label') */
|
|
154
|
+
labelKey: {
|
|
155
|
+
type: StringConstructor;
|
|
156
|
+
default: string;
|
|
157
|
+
};
|
|
158
|
+
/** 子节点字段名(默认 'children') */
|
|
159
|
+
childrenKey: {
|
|
160
|
+
type: StringConstructor;
|
|
161
|
+
default: string;
|
|
162
|
+
};
|
|
163
|
+
/** 受控值(选中叶子的主键;树选择只单选) */
|
|
164
|
+
modelValue: {
|
|
165
|
+
type: StringConstructor;
|
|
166
|
+
default: undefined;
|
|
167
|
+
};
|
|
168
|
+
/** 非受控初始值 */
|
|
169
|
+
defaultValue: {
|
|
170
|
+
type: StringConstructor;
|
|
171
|
+
default: undefined;
|
|
172
|
+
};
|
|
173
|
+
placeholder: {
|
|
174
|
+
type: StringConstructor;
|
|
175
|
+
default: string;
|
|
176
|
+
};
|
|
177
|
+
/** 搜索:`true` 强制开;不给时节点总数多于 8 自动开(与 `Select` 同口径) */
|
|
178
|
+
searchable: {
|
|
179
|
+
type: BooleanConstructor;
|
|
180
|
+
default: boolean;
|
|
181
|
+
};
|
|
182
|
+
/** 自定义命中判定(缺省:标签区分大小写地包含关键词) */
|
|
183
|
+
filterNode: {
|
|
184
|
+
type: PropType<(node: TreeNode, keyword: string) => boolean>;
|
|
185
|
+
default: undefined;
|
|
186
|
+
};
|
|
187
|
+
/** 命中文本是否高亮(默认开) */
|
|
188
|
+
highlight: {
|
|
189
|
+
type: BooleanConstructor;
|
|
190
|
+
default: boolean;
|
|
191
|
+
};
|
|
192
|
+
/** 受控展开集(传了就完全受控;**过滤态下组件强制全展**,不受本 prop 约束) */
|
|
193
|
+
expandedKeys: {
|
|
194
|
+
type: PropType<string[]>;
|
|
195
|
+
default: undefined;
|
|
196
|
+
};
|
|
197
|
+
/** 非受控展开初值 */
|
|
198
|
+
defaultExpandedKeys: {
|
|
199
|
+
type: PropType<string[]>;
|
|
200
|
+
default: () => never[];
|
|
201
|
+
};
|
|
202
|
+
/** 初始展开全部(与 `defaultExpandedKeys` 互斥,它优先) */
|
|
203
|
+
defaultExpandAll: {
|
|
204
|
+
type: BooleanConstructor;
|
|
205
|
+
default: boolean;
|
|
206
|
+
};
|
|
207
|
+
/** 展开集变化回调 */
|
|
208
|
+
onExpandChange: {
|
|
209
|
+
type: PropType<(keys: string[]) => void>;
|
|
210
|
+
default: undefined;
|
|
211
|
+
};
|
|
212
|
+
clearable: {
|
|
213
|
+
type: BooleanConstructor;
|
|
214
|
+
default: boolean;
|
|
215
|
+
};
|
|
216
|
+
disabled: {
|
|
217
|
+
type: BooleanConstructor;
|
|
218
|
+
default: boolean;
|
|
219
|
+
};
|
|
220
|
+
/** 加载中(面板里出一行提示,不渲染树) */
|
|
221
|
+
loading: {
|
|
222
|
+
type: BooleanConstructor;
|
|
223
|
+
default: boolean;
|
|
224
|
+
};
|
|
225
|
+
emptyText: {
|
|
226
|
+
type: StringConstructor;
|
|
227
|
+
default: string;
|
|
228
|
+
};
|
|
229
|
+
/** 尺寸档(24 / 28 / 32) */
|
|
230
|
+
size: {
|
|
231
|
+
type: PropType<ControlSize>;
|
|
232
|
+
default: string;
|
|
233
|
+
};
|
|
234
|
+
/** 校验状态(红 / 黄;与 `Input` 同一词表) */
|
|
235
|
+
status: {
|
|
236
|
+
type: PropType<InputStatus>;
|
|
237
|
+
default: undefined;
|
|
238
|
+
};
|
|
239
|
+
/** 值变更回调(选中叶子的主键) */
|
|
240
|
+
onChange: {
|
|
241
|
+
type: PropType<(v: string) => void>;
|
|
242
|
+
default: undefined;
|
|
243
|
+
};
|
|
244
|
+
/** 搜索词变更(远程换数据用;本地过滤不受它影响) */
|
|
245
|
+
onSearch: {
|
|
246
|
+
type: PropType<(keyword: string) => void>;
|
|
247
|
+
default: undefined;
|
|
248
|
+
};
|
|
249
|
+
}>> & Readonly<{
|
|
250
|
+
"onUpdate:modelValue"?: ((v: string) => any) | undefined;
|
|
251
|
+
}>, {
|
|
252
|
+
size: ControlSize;
|
|
253
|
+
disabled: boolean;
|
|
254
|
+
onChange: (v: string) => void;
|
|
255
|
+
loading: boolean;
|
|
256
|
+
labelKey: string;
|
|
257
|
+
childrenKey: string;
|
|
258
|
+
expandedKeys: string[];
|
|
259
|
+
defaultExpandedKeys: string[];
|
|
260
|
+
defaultExpandAll: boolean;
|
|
261
|
+
onExpandChange: (keys: string[]) => void;
|
|
262
|
+
defaultValue: string;
|
|
263
|
+
placeholder: string;
|
|
264
|
+
modelValue: string;
|
|
265
|
+
status: InputStatus;
|
|
266
|
+
clearable: boolean;
|
|
267
|
+
onSearch: (keyword: string) => void;
|
|
268
|
+
emptyText: string;
|
|
269
|
+
searchable: boolean;
|
|
270
|
+
filterNode: (node: TreeNode, keyword: string) => boolean;
|
|
271
|
+
highlight: boolean;
|
|
272
|
+
}, SlotsType<{
|
|
273
|
+
empty?: () => VNodeChild;
|
|
274
|
+
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { defineComponent, ref, computed, watch, createVNode, createTextVNode, Fragment } from "vue";
|
|
2
|
+
import { Tree } from "../tree/index.js";
|
|
3
|
+
import { SelectPanel } from "./select-panel.js";
|
|
4
|
+
import { SelectTrigger } from "./select-trigger.js";
|
|
5
|
+
import { createSelectPanelId, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
|
|
6
|
+
import { filterTreeNodes, findTreeNode, treeLabel, splitHighlight, treeChildren, treeNodeKey } from "./select-tree-filter.js";
|
|
7
|
+
const SelectTree = /* @__PURE__ */ defineComponent({
|
|
8
|
+
name: "SelectTree",
|
|
9
|
+
inheritAttrs: false,
|
|
10
|
+
props: {
|
|
11
|
+
/** 树数据(与 `Tree` 同一个 `TreeNode` 类型,不另造节点形状) */
|
|
12
|
+
nodes: {
|
|
13
|
+
type: Array,
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
/** 节点主键:字段名或取值函数(与 `Tree` 同义) */
|
|
17
|
+
rowKey: {
|
|
18
|
+
type: [String, Function],
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
/** 标签字段名(默认 'label') */
|
|
22
|
+
labelKey: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: "label"
|
|
25
|
+
},
|
|
26
|
+
/** 子节点字段名(默认 'children') */
|
|
27
|
+
childrenKey: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: "children"
|
|
30
|
+
},
|
|
31
|
+
/** 受控值(选中叶子的主键;树选择只单选) */
|
|
32
|
+
modelValue: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: void 0
|
|
35
|
+
},
|
|
36
|
+
/** 非受控初始值 */
|
|
37
|
+
defaultValue: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: void 0
|
|
40
|
+
},
|
|
41
|
+
placeholder: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: "请选择"
|
|
44
|
+
},
|
|
45
|
+
/** 搜索:`true` 强制开;不给时节点总数多于 8 自动开(与 `Select` 同口径) */
|
|
46
|
+
searchable: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: false
|
|
49
|
+
},
|
|
50
|
+
/** 自定义命中判定(缺省:标签区分大小写地包含关键词) */
|
|
51
|
+
filterNode: {
|
|
52
|
+
type: Function,
|
|
53
|
+
default: void 0
|
|
54
|
+
},
|
|
55
|
+
/** 命中文本是否高亮(默认开) */
|
|
56
|
+
highlight: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: true
|
|
59
|
+
},
|
|
60
|
+
/** 受控展开集(传了就完全受控;**过滤态下组件强制全展**,不受本 prop 约束) */
|
|
61
|
+
expandedKeys: {
|
|
62
|
+
type: Array,
|
|
63
|
+
default: void 0
|
|
64
|
+
},
|
|
65
|
+
/** 非受控展开初值 */
|
|
66
|
+
defaultExpandedKeys: {
|
|
67
|
+
type: Array,
|
|
68
|
+
default: () => []
|
|
69
|
+
},
|
|
70
|
+
/** 初始展开全部(与 `defaultExpandedKeys` 互斥,它优先) */
|
|
71
|
+
defaultExpandAll: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: false
|
|
74
|
+
},
|
|
75
|
+
/** 展开集变化回调 */
|
|
76
|
+
onExpandChange: {
|
|
77
|
+
type: Function,
|
|
78
|
+
default: void 0
|
|
79
|
+
},
|
|
80
|
+
clearable: {
|
|
81
|
+
type: Boolean,
|
|
82
|
+
default: false
|
|
83
|
+
},
|
|
84
|
+
disabled: {
|
|
85
|
+
type: Boolean,
|
|
86
|
+
default: false
|
|
87
|
+
},
|
|
88
|
+
/** 加载中(面板里出一行提示,不渲染树) */
|
|
89
|
+
loading: {
|
|
90
|
+
type: Boolean,
|
|
91
|
+
default: false
|
|
92
|
+
},
|
|
93
|
+
emptyText: {
|
|
94
|
+
type: String,
|
|
95
|
+
default: "暂无数据"
|
|
96
|
+
},
|
|
97
|
+
/** 尺寸档(24 / 28 / 32) */
|
|
98
|
+
size: {
|
|
99
|
+
type: String,
|
|
100
|
+
default: "md"
|
|
101
|
+
},
|
|
102
|
+
/** 校验状态(红 / 黄;与 `Input` 同一词表) */
|
|
103
|
+
status: {
|
|
104
|
+
type: String,
|
|
105
|
+
default: void 0
|
|
106
|
+
},
|
|
107
|
+
/** 值变更回调(选中叶子的主键) */
|
|
108
|
+
onChange: {
|
|
109
|
+
type: Function,
|
|
110
|
+
default: void 0
|
|
111
|
+
},
|
|
112
|
+
/** 搜索词变更(远程换数据用;本地过滤不受它影响) */
|
|
113
|
+
onSearch: {
|
|
114
|
+
type: Function,
|
|
115
|
+
default: void 0
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
emits: {
|
|
119
|
+
"update:modelValue": (v) => true
|
|
120
|
+
},
|
|
121
|
+
slots: Object,
|
|
122
|
+
setup(props, {
|
|
123
|
+
emit,
|
|
124
|
+
slots,
|
|
125
|
+
attrs
|
|
126
|
+
}) {
|
|
127
|
+
const panelId = createSelectPanelId();
|
|
128
|
+
const panelRef = ref(null);
|
|
129
|
+
const inner = ref(props.defaultValue);
|
|
130
|
+
const keyword = ref("");
|
|
131
|
+
const keyOf = (node) => treeNodeKey(node, props.rowKey);
|
|
132
|
+
const childrenOf = (node) => treeChildren(node, props.childrenKey);
|
|
133
|
+
const current = computed(() => props.modelValue ?? inner.value);
|
|
134
|
+
const hasValue = computed(() => current.value !== void 0 && current.value !== "");
|
|
135
|
+
const nodeCount = computed(() => {
|
|
136
|
+
let count = 0;
|
|
137
|
+
const walk = (list) => {
|
|
138
|
+
for (const node of list) {
|
|
139
|
+
count += 1;
|
|
140
|
+
walk(childrenOf(node));
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
walk(props.nodes);
|
|
144
|
+
return count;
|
|
145
|
+
});
|
|
146
|
+
const showSearch = computed(() => props.searchable || nodeCount.value > 8);
|
|
147
|
+
const collectKeys = (list) => list.flatMap((node) => [keyOf(node), ...collectKeys(childrenOf(node))]);
|
|
148
|
+
const innerExpanded = ref(props.defaultExpandAll ? collectKeys(props.nodes) : [...props.defaultExpandedKeys]);
|
|
149
|
+
watch(() => props.defaultExpandedKeys, (next) => {
|
|
150
|
+
if (props.expandedKeys === void 0) innerExpanded.value = [...next];
|
|
151
|
+
});
|
|
152
|
+
const filtered = computed(() => keyword.value ? filterTreeNodes(props.nodes, {
|
|
153
|
+
rowKey: props.rowKey,
|
|
154
|
+
childrenKey: props.childrenKey,
|
|
155
|
+
labelKey: props.labelKey,
|
|
156
|
+
keyword: keyword.value,
|
|
157
|
+
match: props.filterNode
|
|
158
|
+
}) : void 0);
|
|
159
|
+
const visibleNodes = computed(() => filtered.value?.nodes ?? props.nodes);
|
|
160
|
+
const expandedKeys = computed(() => filtered.value ? filtered.value.keys : props.expandedKeys ?? innerExpanded.value);
|
|
161
|
+
const handleExpandChange = (keys) => {
|
|
162
|
+
if (props.expandedKeys === void 0) innerExpanded.value = keys;
|
|
163
|
+
props.onExpandChange?.(keys);
|
|
164
|
+
};
|
|
165
|
+
const displayLabel = computed(() => {
|
|
166
|
+
if (!hasValue.value) return void 0;
|
|
167
|
+
const found = findTreeNode(props.nodes, String(current.value), {
|
|
168
|
+
rowKey: props.rowKey,
|
|
169
|
+
childrenKey: props.childrenKey
|
|
170
|
+
});
|
|
171
|
+
return found ? treeLabel(found, props.labelKey) : String(current.value);
|
|
172
|
+
});
|
|
173
|
+
const commit = (value) => {
|
|
174
|
+
if (props.modelValue === void 0) inner.value = value;
|
|
175
|
+
emit("update:modelValue", value);
|
|
176
|
+
props.onChange?.(value);
|
|
177
|
+
};
|
|
178
|
+
const hide = () => hidePanel(panelRef.value);
|
|
179
|
+
const toggleExpand = (key) => {
|
|
180
|
+
const next = new Set(expandedKeys.value);
|
|
181
|
+
if (next.has(key)) next.delete(key);
|
|
182
|
+
else next.add(key);
|
|
183
|
+
handleExpandChange([...next]);
|
|
184
|
+
};
|
|
185
|
+
const pick = (node) => {
|
|
186
|
+
if (childrenOf(node).length > 0) {
|
|
187
|
+
toggleExpand(keyOf(node));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
commit(keyOf(node));
|
|
191
|
+
hide();
|
|
192
|
+
};
|
|
193
|
+
const clear = () => commit("");
|
|
194
|
+
const onTriggerKeydown = createTriggerKeydown({
|
|
195
|
+
clearable: props.clearable,
|
|
196
|
+
disabled: props.disabled,
|
|
197
|
+
clear,
|
|
198
|
+
openPanel: () => showPanel(panelRef.value)
|
|
199
|
+
/* 有意不给 `focusFirst`:树的行是 `div`、没有 tabindex,面板内不做 ↑↓ 行导航 */
|
|
200
|
+
});
|
|
201
|
+
const renderHitLabel = (node) => createVNode(Fragment, null, [splitHighlight(treeLabel(node, props.labelKey), keyword.value).map((segment, index) => segment.hit ? createVNode("span", {
|
|
202
|
+
"key": index,
|
|
203
|
+
"class": "mh-select__hit"
|
|
204
|
+
}, [segment.text]) : segment.text)]);
|
|
205
|
+
return () => {
|
|
206
|
+
const highlightLabel = props.highlight && !!keyword.value ? renderHitLabel : void 0;
|
|
207
|
+
return createVNode("div", {
|
|
208
|
+
"class": ["mh-popup-host", attrs.class].filter(Boolean).join(" "),
|
|
209
|
+
"style": attrs.style
|
|
210
|
+
}, [createVNode(SelectTrigger, {
|
|
211
|
+
"panelId": panelId,
|
|
212
|
+
"size": props.size,
|
|
213
|
+
"status": props.status,
|
|
214
|
+
"disabled": props.disabled,
|
|
215
|
+
"clearable": props.clearable,
|
|
216
|
+
"hasValue": hasValue.value,
|
|
217
|
+
"placeholder": props.placeholder,
|
|
218
|
+
"label": displayLabel.value,
|
|
219
|
+
"onKeydown": onTriggerKeydown,
|
|
220
|
+
"onClear": clear
|
|
221
|
+
}, null), createVNode(SelectPanel, {
|
|
222
|
+
"id": panelId,
|
|
223
|
+
"elementRef": panelRef,
|
|
224
|
+
"searchable": showSearch.value,
|
|
225
|
+
"keyword": keyword.value,
|
|
226
|
+
"onKeyword": (value) => {
|
|
227
|
+
keyword.value = value;
|
|
228
|
+
props.onSearch?.(value);
|
|
229
|
+
}
|
|
230
|
+
}, {
|
|
231
|
+
default: () => {
|
|
232
|
+
if (props.loading) {
|
|
233
|
+
return createVNode("span", {
|
|
234
|
+
"class": "mh-select__option mh-select__option--hint"
|
|
235
|
+
}, [createTextVNode("加载中…")]);
|
|
236
|
+
}
|
|
237
|
+
if (visibleNodes.value.length === 0) {
|
|
238
|
+
const custom = slots.empty?.();
|
|
239
|
+
return custom !== void 0 && custom !== null ? custom : createVNode("span", {
|
|
240
|
+
"class": "mh-select__option mh-select__option--empty"
|
|
241
|
+
}, [props.emptyText]);
|
|
242
|
+
}
|
|
243
|
+
return createVNode("div", {
|
|
244
|
+
"class": "mh-select__tree"
|
|
245
|
+
}, [createVNode(Tree, {
|
|
246
|
+
"nodes": visibleNodes.value,
|
|
247
|
+
"rowKey": props.rowKey,
|
|
248
|
+
"labelKey": props.labelKey,
|
|
249
|
+
"childrenKey": props.childrenKey,
|
|
250
|
+
"selected": current.value === "" ? void 0 : current.value,
|
|
251
|
+
"expandedKeys": expandedKeys.value,
|
|
252
|
+
"onExpandChange": handleExpandChange,
|
|
253
|
+
"onSelect": pick,
|
|
254
|
+
"renderLabel": highlightLabel
|
|
255
|
+
}, null)]);
|
|
256
|
+
}
|
|
257
|
+
})]);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
export {
|
|
262
|
+
SelectTree
|
|
263
|
+
};
|