@manohub/ui 0.9.0 → 0.10.1
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 +13 -7
- package/dist/components/checkbox/index.js +5 -1
- package/dist/components/code-block/index.js +18 -11
- package/dist/components/filter/index.d.ts +8 -2
- package/dist/components/filter/index.js +11 -20
- package/dist/components/form/index.d.ts +39 -0
- package/dist/components/form/index.js +6 -0
- package/dist/components/input/index.d.ts +76 -20
- package/dist/components/input/index.js +64 -15
- package/dist/components/radio/index.js +29 -3
- package/dist/components/search/index.js +37 -35
- package/dist/components/select/index.d.ts +5 -2
- package/dist/components/select/index.js +3 -6
- package/dist/components/select/select-list.d.ts +2 -0
- package/dist/components/select/select-panel.d.ts +9 -4
- package/dist/components/select/select-panel.js +4 -2
- package/dist/components/select/select-tree.d.ts +5 -3
- package/dist/components/select/select-tree.js +3 -15
- package/dist/components/select/select-trigger.d.ts +4 -2
- package/dist/components/select/select-trigger.js +9 -4
- package/dist/components/table/index.d.ts +19 -8
- package/dist/components/table/index.js +3 -3
- package/dist/components/text/index.d.ts +4 -3
- package/dist/components/toast/index.js +16 -1
- package/dist/styles/components/avatar.css +4 -1
- package/dist/styles/components/badge.css +3 -1
- package/dist/styles/components/button.css +7 -2
- package/dist/styles/components/filter.css +22 -34
- package/dist/styles/components/input.css +37 -1
- package/dist/styles/components/input.tokens.css +5 -3
- package/dist/styles/components/pagination.css +8 -2
- package/dist/styles/components/select.css +39 -10
- package/dist/styles/components/select.tokens.css +4 -3
- package/dist/styles/components/steps.tokens.css +5 -3
- package/dist/styles/components/table.css +33 -22
- package/dist/styles/components/table.tokens.css +6 -6
- package/dist/styles/components/tag.css +7 -3
- package/dist/styles/components/text.css +10 -0
- package/dist/styles/components/toast.css +35 -6
- package/dist/styles/components/upload.css +1 -0
- package/dist/styles/components/upload.tokens.css +1 -1
- package/dist/styles/index.css +5 -4
- package/dist/styles/reset.css +52 -167
- package/package.json +2 -2
- package/dist/styles/components/filter.tokens.css +0 -14
|
@@ -14,6 +14,13 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
14
14
|
type: String,
|
|
15
15
|
default: ""
|
|
16
16
|
},
|
|
17
|
+
/**
|
|
18
|
+
* 占位文案。两种形态都消费它:
|
|
19
|
+
* - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
|
|
20
|
+
* - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
|
|
21
|
+
* (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
|
|
22
|
+
* 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
|
|
23
|
+
*/
|
|
17
24
|
placeholder: {
|
|
18
25
|
type: String,
|
|
19
26
|
default: void 0
|
|
@@ -71,7 +78,7 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
71
78
|
type: Number,
|
|
72
79
|
default: void 0
|
|
73
80
|
},
|
|
74
|
-
/**
|
|
81
|
+
/** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
|
|
75
82
|
showCount: {
|
|
76
83
|
type: Boolean,
|
|
77
84
|
default: false
|
|
@@ -95,7 +102,17 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
95
102
|
* 整件的**表壳、附加位与状态规则一并复用**,只把「控件元素」从 `<input>` 换成 `<button>`:
|
|
96
103
|
* 于是 `Select` 这类件能拿到输入框的框与内距、内建清除位、右侧图标位与三态反馈,同时保留
|
|
97
104
|
* 原生 `<button>` 才有的语义(如 `popovertarget` 触发 popover —— 该属性只认 button)。
|
|
98
|
-
* 缺省 `as="input"` 的渲染路径除本组 prop 外零变化。
|
|
105
|
+
* 缺省 `as="input"` 的渲染路径除本组 prop 外零变化。
|
|
106
|
+
*
|
|
107
|
+
* **单行契约(与输入框同源,面层承担)**:控件是单行盒,值再长也不换行(超出走省略号)
|
|
108
|
+
* —— 见 `styles/components/input.css` 的 `.mh-input--trigger`。故 `default` 插槽里给**裸文本**
|
|
109
|
+
* 也安全,不需要自己再包一层截断 span。
|
|
110
|
+
*
|
|
111
|
+
* **无值时的占位**:`<button>` 没有 `::placeholder` 伪元素可用,故 `placeholder` 在这个形态下
|
|
112
|
+
* 由本件渲染成一个 span(`.mh-input__placeholder`,色档与原生占位一致);判据是
|
|
113
|
+
* `hasValue === false && placeholder` **同时**成立 —— 两者缺一即沿用消费方给的 `default` 插槽。
|
|
114
|
+
* 这样「有值渲染值、无值渲染占位」这件例行事的文案与切换都由输入面统一,复合件不必各画一遍
|
|
115
|
+
* (`Select` 早期为自己的值区单画过一份,见 `select-trigger.tsx`)。 */
|
|
99
116
|
/** 控件元素形态:`input` 文本输入(缺省)/ `button` 触发器 */
|
|
100
117
|
as: {
|
|
101
118
|
type: String,
|
|
@@ -106,12 +123,14 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
106
123
|
type: String,
|
|
107
124
|
default: void 0
|
|
108
125
|
},
|
|
109
|
-
/** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown`
|
|
126
|
+
/** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
|
|
127
|
+
* 与透传的 attrs 撞名时以本 prop 为准 */
|
|
110
128
|
controlAttrs: {
|
|
111
129
|
type: Object,
|
|
112
130
|
default: void 0
|
|
113
131
|
},
|
|
114
|
-
/**
|
|
132
|
+
/** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
|
|
133
|
+
* 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
|
|
115
134
|
hasValue: {
|
|
116
135
|
type: Boolean,
|
|
117
136
|
default: void 0
|
|
@@ -129,6 +148,13 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
129
148
|
const inner = ref(props.defaultValue);
|
|
130
149
|
const current = computed(() => props.modelValue ?? inner.value);
|
|
131
150
|
const revealed = ref(false);
|
|
151
|
+
const forwardAttrs = computed(() => {
|
|
152
|
+
const rest = {};
|
|
153
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
154
|
+
if (key !== "class" && key !== "style") rest[key] = value;
|
|
155
|
+
}
|
|
156
|
+
return rest;
|
|
157
|
+
});
|
|
132
158
|
const visualStatus = computed(() => props.status ?? (props.invalid ? "error" : void 0));
|
|
133
159
|
const isInvalid = computed(() => visualStatus.value === "error");
|
|
134
160
|
const commit = (value) => {
|
|
@@ -149,6 +175,8 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
149
175
|
const isTrigger = computed(() => props.as === "button");
|
|
150
176
|
const controlClasses = computed(() => ["mh-input", isTrigger.value ? "mh-input--trigger" : "", props.size === "md" ? "" : `mh-input--${props.size}`, props.bordered ? "" : "mh-input--borderless", visualStatus.value ? `is-${visualStatus.value}` : "", props.disabled ? "is-disabled" : "", props.controlClass].filter(Boolean).join(" "));
|
|
151
177
|
const showClear = computed(() => props.clearable && (props.hasValue ?? !!current.value));
|
|
178
|
+
const showCounter = computed(() => !isTrigger.value && props.showCount && props.maxlength !== void 0);
|
|
179
|
+
const showPlaceholder = computed(() => isTrigger.value && props.hasValue === false && !!props.placeholder);
|
|
152
180
|
return () => createVNode("div", {
|
|
153
181
|
"class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
|
|
154
182
|
"style": attrs.style
|
|
@@ -160,12 +188,17 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
160
188
|
本件的 Enter / Delete 是**输入框**语义,触发器有自己的一套(如 `↓` 开面板),不在此套用。 */
|
|
161
189
|
createVNode("button", mergeProps({
|
|
162
190
|
"type": "button"
|
|
163
|
-
}, props.controlAttrs, {
|
|
191
|
+
}, forwardAttrs.value, props.controlAttrs, {
|
|
164
192
|
"class": controlClasses.value,
|
|
165
193
|
"disabled": props.disabled,
|
|
166
194
|
"aria-invalid": isInvalid.value ? "true" : void 0
|
|
167
|
-
}), [
|
|
168
|
-
|
|
195
|
+
}), [showPlaceholder.value ? (
|
|
196
|
+
/* 触发器没有 `::placeholder`,占位文案由本件渲染(色见 `.mh-input__placeholder`) */
|
|
197
|
+
createVNode("span", {
|
|
198
|
+
"class": "mh-input__placeholder"
|
|
199
|
+
}, [props.placeholder])
|
|
200
|
+
) : slots.default ? slots.default() : null])
|
|
201
|
+
) : createVNode("input", mergeProps(forwardAttrs.value, {
|
|
169
202
|
"class": controlClasses.value,
|
|
170
203
|
"type": inputType.value,
|
|
171
204
|
"value": current.value,
|
|
@@ -177,7 +210,7 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
177
210
|
"aria-invalid": isInvalid.value ? "true" : void 0,
|
|
178
211
|
"onInput": onInput,
|
|
179
212
|
"onKeydown": onKeydown
|
|
180
|
-
}, null), showClear.value ? createVNode("button", {
|
|
213
|
+
}), null), showClear.value ? createVNode("button", {
|
|
181
214
|
"type": "button",
|
|
182
215
|
"class": "mh-input__clear",
|
|
183
216
|
"aria-label": "清除",
|
|
@@ -199,9 +232,9 @@ const Input = /* @__PURE__ */ defineComponent({
|
|
|
199
232
|
"class": "mh-input__eye-icon"
|
|
200
233
|
}, null)]) : null, slots.suffix ? createVNode("span", {
|
|
201
234
|
"class": "mh-input__suffix"
|
|
202
|
-
}, [slots.suffix()]) : null,
|
|
235
|
+
}, [slots.suffix()]) : null, showCounter.value ? createVNode("span", {
|
|
203
236
|
"class": "mh-input__counter"
|
|
204
|
-
}, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength
|
|
237
|
+
}, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
|
|
205
238
|
}
|
|
206
239
|
});
|
|
207
240
|
const Textarea = /* @__PURE__ */ defineComponent({
|
|
@@ -257,6 +290,7 @@ const Textarea = /* @__PURE__ */ defineComponent({
|
|
|
257
290
|
type: Number,
|
|
258
291
|
default: void 0
|
|
259
292
|
},
|
|
293
|
+
/** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
|
|
260
294
|
showCount: {
|
|
261
295
|
type: Boolean,
|
|
262
296
|
default: false
|
|
@@ -279,6 +313,14 @@ const Textarea = /* @__PURE__ */ defineComponent({
|
|
|
279
313
|
}) {
|
|
280
314
|
const inner = ref(props.defaultValue);
|
|
281
315
|
const current = computed(() => props.modelValue ?? inner.value);
|
|
316
|
+
const forwardAttrs = computed(() => {
|
|
317
|
+
const rest = {};
|
|
318
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
319
|
+
if (key !== "class" && key !== "style") rest[key] = value;
|
|
320
|
+
}
|
|
321
|
+
return rest;
|
|
322
|
+
});
|
|
323
|
+
const showCounter = computed(() => props.showCount && props.maxlength !== void 0);
|
|
282
324
|
const visualStatus = computed(() => props.status ?? (props.invalid ? "error" : void 0));
|
|
283
325
|
const isInvalid = computed(() => visualStatus.value === "error");
|
|
284
326
|
const autoRows = computed(() => {
|
|
@@ -300,7 +342,7 @@ const Textarea = /* @__PURE__ */ defineComponent({
|
|
|
300
342
|
return () => createVNode("div", {
|
|
301
343
|
"class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
|
|
302
344
|
"style": attrs.style
|
|
303
|
-
}, [createVNode("textarea", {
|
|
345
|
+
}, [createVNode("textarea", mergeProps(forwardAttrs.value, {
|
|
304
346
|
"class": ["mh-input", "mh-input--textarea", props.size === "md" ? "" : `mh-input--${props.size}`, props.bordered ? "" : "mh-input--borderless", visualStatus.value ? `is-${visualStatus.value}` : "", props.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
305
347
|
"rows": autoRows.value,
|
|
306
348
|
"value": current.value,
|
|
@@ -311,9 +353,9 @@ const Textarea = /* @__PURE__ */ defineComponent({
|
|
|
311
353
|
"autofocus": props.autofocus,
|
|
312
354
|
"aria-invalid": isInvalid.value ? "true" : void 0,
|
|
313
355
|
"onInput": onInput
|
|
314
|
-
}, null),
|
|
356
|
+
}), null), showCounter.value ? createVNode("span", {
|
|
315
357
|
"class": "mh-input__counter"
|
|
316
|
-
}, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength
|
|
358
|
+
}, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
|
|
317
359
|
}
|
|
318
360
|
});
|
|
319
361
|
const InputDisplay = /* @__PURE__ */ defineComponent({
|
|
@@ -330,10 +372,17 @@ const InputDisplay = /* @__PURE__ */ defineComponent({
|
|
|
330
372
|
slots,
|
|
331
373
|
attrs
|
|
332
374
|
}) {
|
|
333
|
-
|
|
375
|
+
const forwardAttrs = computed(() => {
|
|
376
|
+
const rest = {};
|
|
377
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
378
|
+
if (key !== "class" && key !== "style") rest[key] = value;
|
|
379
|
+
}
|
|
380
|
+
return rest;
|
|
381
|
+
});
|
|
382
|
+
return () => createVNode("span", mergeProps(forwardAttrs.value, {
|
|
334
383
|
"class": ["mh-input--display", attrs.class].filter(Boolean).join(" "),
|
|
335
384
|
"style": attrs.style
|
|
336
|
-
}, [slots.default ? slots.default() : props.value]);
|
|
385
|
+
}), [slots.default ? slots.default() : props.value]);
|
|
337
386
|
}
|
|
338
387
|
});
|
|
339
388
|
Object.assign(Input, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, createVNode, ref, computed } from "vue";
|
|
1
|
+
import { defineComponent, createVNode, ref, nextTick, computed } from "vue";
|
|
2
2
|
const Radio = /* @__PURE__ */ defineComponent({
|
|
3
3
|
name: "Radio",
|
|
4
4
|
inheritAttrs: false,
|
|
@@ -136,7 +136,17 @@ const RadioGroup = /* @__PURE__ */ defineComponent({
|
|
|
136
136
|
props.onChange?.(value);
|
|
137
137
|
}
|
|
138
138
|
);
|
|
139
|
+
const rootRef = ref(null);
|
|
140
|
+
const syncNative = () => {
|
|
141
|
+
const root = rootRef.value;
|
|
142
|
+
if (!root) return;
|
|
143
|
+
root.querySelectorAll(".mh-radio__input").forEach((el) => {
|
|
144
|
+
const want = String(current.value) === el.value;
|
|
145
|
+
if (el.checked !== want) el.checked = want;
|
|
146
|
+
});
|
|
147
|
+
};
|
|
139
148
|
return () => createVNode("div", {
|
|
149
|
+
"ref": rootRef,
|
|
140
150
|
"class": ["mh-radio-group", props.vertical ? "mh-radio-group--vertical" : void 0, attrs.class].filter(Boolean).join(" "),
|
|
141
151
|
"style": attrs.style
|
|
142
152
|
}, [slots.default ? slots.default({
|
|
@@ -150,7 +160,10 @@ const RadioGroup = /* @__PURE__ */ defineComponent({
|
|
|
150
160
|
"checked": option.value === current.value,
|
|
151
161
|
"disabled": props.disabled || option.disabled,
|
|
152
162
|
"size": props.size,
|
|
153
|
-
"onChange":
|
|
163
|
+
"onChange": (value) => {
|
|
164
|
+
select(value);
|
|
165
|
+
void nextTick(syncNative);
|
|
166
|
+
}
|
|
154
167
|
}, null))]);
|
|
155
168
|
}
|
|
156
169
|
});
|
|
@@ -206,7 +219,17 @@ const RadioCard = /* @__PURE__ */ defineComponent({
|
|
|
206
219
|
props.onChange?.(value);
|
|
207
220
|
}
|
|
208
221
|
);
|
|
222
|
+
const rootRef = ref(null);
|
|
223
|
+
const syncNative = () => {
|
|
224
|
+
const root = rootRef.value;
|
|
225
|
+
if (!root) return;
|
|
226
|
+
root.querySelectorAll(".mh-radio-card__input").forEach((el) => {
|
|
227
|
+
const want = current.value === el.value;
|
|
228
|
+
if (el.checked !== want) el.checked = want;
|
|
229
|
+
});
|
|
230
|
+
};
|
|
209
231
|
return () => createVNode("div", {
|
|
232
|
+
"ref": rootRef,
|
|
210
233
|
"class": ["mh-radio-card", props.columns > 1 ? "mh-radio-card--grid" : "", attrs.class].filter(Boolean).join(" "),
|
|
211
234
|
"style": props.columns > 1 ? {
|
|
212
235
|
...attrs.style,
|
|
@@ -228,7 +251,10 @@ const RadioCard = /* @__PURE__ */ defineComponent({
|
|
|
228
251
|
"value": String(item.value),
|
|
229
252
|
"checked": isChecked,
|
|
230
253
|
"disabled": isDisabled,
|
|
231
|
-
"onChange": () =>
|
|
254
|
+
"onChange": () => {
|
|
255
|
+
select(item.value);
|
|
256
|
+
void nextTick(syncNative);
|
|
257
|
+
}
|
|
232
258
|
}, null), createVNode("span", {
|
|
233
259
|
"class": "mh-radio-card__radio",
|
|
234
260
|
"aria-hidden": "true"
|
|
@@ -76,44 +76,46 @@ const Search = /* @__PURE__ */ defineComponent({
|
|
|
76
76
|
const iconAction = computed(() => props.action === "icon");
|
|
77
77
|
const showAction = computed(() => iconAction.value || props.searchText !== void 0);
|
|
78
78
|
const actionLabel = computed(() => props.searchText ?? "搜索");
|
|
79
|
-
return () =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, {
|
|
92
|
-
default: () => [iconAction.value ? {} : {
|
|
79
|
+
return () => {
|
|
80
|
+
const field = {
|
|
81
|
+
class: "mh-search__input",
|
|
82
|
+
modelValue: current.value,
|
|
83
|
+
size: props.size,
|
|
84
|
+
disabled: props.disabled,
|
|
85
|
+
placeholder: props.placeholder,
|
|
86
|
+
clearable: props.clearable,
|
|
87
|
+
onChange: commit,
|
|
88
|
+
onEnter: search
|
|
89
|
+
};
|
|
90
|
+
const fieldNode = iconAction.value ? createVNode(Input, field, null) : createVNode(Input, field, {
|
|
93
91
|
prefix: () => createVNode(Icon, {
|
|
94
92
|
"name": "search"
|
|
95
93
|
}, null)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
94
|
+
});
|
|
95
|
+
return createVNode("div", {
|
|
96
|
+
"class": ["mh-search", attrs.class].filter(Boolean).join(" "),
|
|
97
|
+
"style": attrs.style
|
|
98
|
+
}, [fieldNode, showAction.value ? iconAction.value ? createVNode(Button, {
|
|
99
|
+
"class": "mh-search__btn",
|
|
100
|
+
"variant": "primary",
|
|
101
|
+
"size": props.size,
|
|
102
|
+
"icon": "search",
|
|
103
|
+
"title": actionLabel.value,
|
|
104
|
+
"disabled": props.disabled,
|
|
105
|
+
"loading": props.loading,
|
|
106
|
+
"onClick": search
|
|
107
|
+
}, null) : createVNode(Button, {
|
|
108
|
+
"class": "mh-search__btn",
|
|
109
|
+
"variant": "primary",
|
|
110
|
+
"size": props.size,
|
|
111
|
+
"icon": "search",
|
|
112
|
+
"disabled": props.disabled,
|
|
113
|
+
"loading": props.loading,
|
|
114
|
+
"onClick": search
|
|
115
|
+
}, {
|
|
116
|
+
default: () => props.searchText
|
|
117
|
+
}) : null]);
|
|
118
|
+
};
|
|
117
119
|
}
|
|
118
120
|
});
|
|
119
121
|
export {
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
* (不做 JS 定位、不做 resize 监听)。本件只补原生做不到的:选中值回填、多选数组的增删与收纳、
|
|
33
33
|
* 搜索过滤与「创建」项、以及键盘的下拉导航(↑↓ 移动高亮行)。
|
|
34
34
|
*
|
|
35
|
+
* **面板有高度上限**:`--ui-select-panel-max-height`(默认 320px,钉在 `select.css` 的面板根上)——
|
|
36
|
+
* 超出即面板内容区(`.mh-select__body`)滚动、搜索行常驻;列表与 `SelectTree` 两形态共用这一份。
|
|
37
|
+
*
|
|
35
38
|
* ## 状态三源等价
|
|
36
39
|
*
|
|
37
40
|
* 选中行 = `[data-state='selected']`(组件下发)/ 原生 `input:checked`(静态页零 JS 路径)/
|
|
@@ -87,7 +90,7 @@ export declare const Select: import("vue").DefineComponent<import("vue").Extract
|
|
|
87
90
|
type: StringConstructor;
|
|
88
91
|
default: undefined;
|
|
89
92
|
};
|
|
90
|
-
/**
|
|
93
|
+
/** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
|
|
91
94
|
searchable: {
|
|
92
95
|
type: BooleanConstructor;
|
|
93
96
|
default: boolean;
|
|
@@ -176,7 +179,7 @@ export declare const Select: import("vue").DefineComponent<import("vue").Extract
|
|
|
176
179
|
type: StringConstructor;
|
|
177
180
|
default: undefined;
|
|
178
181
|
};
|
|
179
|
-
/**
|
|
182
|
+
/** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
|
|
180
183
|
searchable: {
|
|
181
184
|
type: BooleanConstructor;
|
|
182
185
|
default: boolean;
|
|
@@ -41,7 +41,7 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
41
41
|
type: String,
|
|
42
42
|
default: void 0
|
|
43
43
|
},
|
|
44
|
-
/**
|
|
44
|
+
/** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
|
|
45
45
|
searchable: {
|
|
46
46
|
type: Boolean,
|
|
47
47
|
default: false
|
|
@@ -122,7 +122,6 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
122
122
|
return list;
|
|
123
123
|
});
|
|
124
124
|
const selectedOptions = computed(() => flatOptions.value.filter((option) => selectedValues.value.includes(option.value)));
|
|
125
|
-
const showSearch = computed(() => props.searchable || flatOptions.value.length > 8);
|
|
126
125
|
const matched = (option, kw) => props.filterOption ? props.filterOption(option, kw) : option.label.includes(kw);
|
|
127
126
|
const visibleOptions = computed(() => {
|
|
128
127
|
const kw = keyword.value;
|
|
@@ -211,13 +210,11 @@ const Select = /* @__PURE__ */ defineComponent({
|
|
|
211
210
|
"class": "mh-select__chip"
|
|
212
211
|
}, [option.label])), foldedCount.value > 0 ? createVNode("span", {
|
|
213
212
|
"class": "mh-select__chip mh-select__chip--fold"
|
|
214
|
-
}, [foldText.value]) : null
|
|
215
|
-
"class": "mh-select__value mh-select__value--placeholder"
|
|
216
|
-
}, [props.placeholder]) : null]) : void 0
|
|
213
|
+
}, [foldText.value]) : null]) : void 0
|
|
217
214
|
}, null), createVNode(SelectPanel, {
|
|
218
215
|
"id": panelId,
|
|
219
216
|
"elementRef": panelRef,
|
|
220
|
-
"searchable":
|
|
217
|
+
"searchable": props.searchable,
|
|
221
218
|
"keyword": keyword.value,
|
|
222
219
|
"onKeyword": (value) => {
|
|
223
220
|
keyword.value = value;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* 它拥有「面板里那一撮选项」的全部形态与状态行:平铺选项 / 分组(组标题不可点)/ 「创建」行 /
|
|
5
5
|
* 空态行 / 加载行。**不含面板壳与搜索行**(那是 `SelectPanel` 的事),也不持有任何状态 ——
|
|
6
6
|
* 过滤结果、选中值、是否可创建统统由消费方算好喂进来,本件只渲染。
|
|
7
|
+
* 行由面板壳装在**内容区**(`.mh-select__body`,面板里唯一的滚动容器)里,限高与滚动都归面层,
|
|
8
|
+
* 本件不写第二个滚动容器。
|
|
7
9
|
*
|
|
8
10
|
* 选项是真实 `<button role="option">`(不是 div + JS 高亮):键盘可达、`:focus-visible` 可直接
|
|
9
11
|
* 承载「高亮行」这层语义,于是不需要再维护一份 JS 高亮下标 —— **焦点即高亮**,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SelectPanel —— 选择器族的**面板壳内件**(`Select` 与 `SelectTree` 共用,不对外导出)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 它只拥有四样东西:**原生 popover 元素**(开合 / Esc / 点外部关闭 / 顶层渲染 / 定位全归浏览器)、
|
|
5
|
+
* **搜索行**(面板顶部那一个输入框)、**内容区**(`.mh-select__body`,面板里唯一的滚动容器)
|
|
6
|
+
* 与**内容槽**(列表体或树体)。
|
|
6
7
|
* 「面板里装什么」不在本件 —— 这正是让列表与树共用同一份面板皮肤的原因。
|
|
7
8
|
*
|
|
9
|
+
* **限高在面层**:面板根上钉着 `--ui-select-panel-max-height`(`select.css`),超出即内容区滚动、
|
|
10
|
+
* 搜索行常驻不滚。内容区这一层因此是**结构性的**(不是可有可无的包裹):少了它,长列表 / 长树
|
|
11
|
+
* 会把面板一路撑高(实测 60 条选项 2212px),搜索行也会跟着滚走。本件只负责把它渲染出来。
|
|
12
|
+
*
|
|
8
13
|
* 组合类名与服务端三条属性都是**操作层的组合**,不是本件新造的浮层:
|
|
9
14
|
* `class="mh-popup mh-select__panel mh-popup--anchored"`(状态根 + 本族皮肤 + 显式锚档)、
|
|
10
15
|
* `data-side` / `data-align`(`popup.css` 的定位档)、`popover="auto"`(原生轻 dismiss)。
|
|
@@ -27,7 +32,7 @@ export declare const SelectPanel: import("vue").DefineComponent<import("vue").Ex
|
|
|
27
32
|
type: PropType<ElementRef>;
|
|
28
33
|
default: undefined;
|
|
29
34
|
};
|
|
30
|
-
/**
|
|
35
|
+
/** 是否渲染搜索行(**只认消费方显式开启**:列表形态与树形态同口径,件不按数据量自动决断) */
|
|
31
36
|
searchable: {
|
|
32
37
|
type: BooleanConstructor;
|
|
33
38
|
default: boolean;
|
|
@@ -66,7 +71,7 @@ export declare const SelectPanel: import("vue").DefineComponent<import("vue").Ex
|
|
|
66
71
|
type: PropType<ElementRef>;
|
|
67
72
|
default: undefined;
|
|
68
73
|
};
|
|
69
|
-
/**
|
|
74
|
+
/** 是否渲染搜索行(**只认消费方显式开启**:列表形态与树形态同口径,件不按数据量自动决断) */
|
|
70
75
|
searchable: {
|
|
71
76
|
type: BooleanConstructor;
|
|
72
77
|
default: boolean;
|
|
@@ -13,7 +13,7 @@ const SelectPanel = /* @__PURE__ */ defineComponent({
|
|
|
13
13
|
type: Object,
|
|
14
14
|
default: void 0
|
|
15
15
|
},
|
|
16
|
-
/**
|
|
16
|
+
/** 是否渲染搜索行(**只认消费方显式开启**:列表形态与树形态同口径,件不按数据量自动决断) */
|
|
17
17
|
searchable: {
|
|
18
18
|
type: Boolean,
|
|
19
19
|
default: false
|
|
@@ -64,7 +64,9 @@ const SelectPanel = /* @__PURE__ */ defineComponent({
|
|
|
64
64
|
"value": props.keyword,
|
|
65
65
|
"placeholder": props.searchPlaceholder,
|
|
66
66
|
"onInput": (event) => props.onKeyword?.(event.target.value)
|
|
67
|
-
}, null)]) : null,
|
|
67
|
+
}, null)]) : null, createVNode("div", {
|
|
68
|
+
"class": "mh-select__body"
|
|
69
|
+
}, [slots.default?.()])]);
|
|
68
70
|
}
|
|
69
71
|
});
|
|
70
72
|
export {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* 2. **本地过滤树**:搜索行在场时按关键词过滤,命中节点的**祖先链保留**、父节点自身命中则
|
|
11
11
|
* **整支保留**,并强制全展到命中层(算法与切分都是纯函数,见 `select-tree-filter.ts`);
|
|
12
12
|
* 命中文本按面层的 `.mh-select__hit` 高亮(靠 `Tree` 的可选 `renderLabel` 钩子落进标签槽)。
|
|
13
|
-
* 3.
|
|
13
|
+
* 3. **面板内限高滚动**:限高归面板(`.mh-select__panel` 的 `--ui-select-panel-max-height`)、
|
|
14
|
+
* 溢出由面板内容区(`.mh-select__body`)承担;本件只把树包在 `.mh-select__tree` 里给内距
|
|
15
|
+
* —— 限高不由形态自持,列表形态共用同一份。
|
|
14
16
|
*
|
|
15
17
|
* ## 与 `Select` 的三处有意差异(都是「树不是列表」决定的)
|
|
16
18
|
*
|
|
@@ -65,7 +67,7 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
|
|
|
65
67
|
type: StringConstructor;
|
|
66
68
|
default: string;
|
|
67
69
|
};
|
|
68
|
-
/**
|
|
70
|
+
/** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
|
|
69
71
|
searchable: {
|
|
70
72
|
type: BooleanConstructor;
|
|
71
73
|
default: boolean;
|
|
@@ -174,7 +176,7 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
|
|
|
174
176
|
type: StringConstructor;
|
|
175
177
|
default: string;
|
|
176
178
|
};
|
|
177
|
-
/**
|
|
179
|
+
/** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
|
|
178
180
|
searchable: {
|
|
179
181
|
type: BooleanConstructor;
|
|
180
182
|
default: boolean;
|
|
@@ -3,7 +3,7 @@ import { Tree } from "../tree/index.js";
|
|
|
3
3
|
import { SelectPanel } from "./select-panel.js";
|
|
4
4
|
import { SelectTrigger } from "./select-trigger.js";
|
|
5
5
|
import { createSelectPanelId, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
|
|
6
|
-
import { filterTreeNodes, findTreeNode, treeLabel, splitHighlight,
|
|
6
|
+
import { filterTreeNodes, findTreeNode, treeLabel, splitHighlight, treeNodeKey, treeChildren } from "./select-tree-filter.js";
|
|
7
7
|
const SelectTree = /* @__PURE__ */ defineComponent({
|
|
8
8
|
name: "SelectTree",
|
|
9
9
|
inheritAttrs: false,
|
|
@@ -42,7 +42,7 @@ const SelectTree = /* @__PURE__ */ defineComponent({
|
|
|
42
42
|
type: String,
|
|
43
43
|
default: "请选择"
|
|
44
44
|
},
|
|
45
|
-
/**
|
|
45
|
+
/** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
|
|
46
46
|
searchable: {
|
|
47
47
|
type: Boolean,
|
|
48
48
|
default: false
|
|
@@ -132,18 +132,6 @@ const SelectTree = /* @__PURE__ */ defineComponent({
|
|
|
132
132
|
const childrenOf = (node) => treeChildren(node, props.childrenKey);
|
|
133
133
|
const current = computed(() => props.modelValue ?? inner.value);
|
|
134
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
135
|
const collectKeys = (list) => list.flatMap((node) => [keyOf(node), ...collectKeys(childrenOf(node))]);
|
|
148
136
|
const innerExpanded = ref(props.defaultExpandAll ? collectKeys(props.nodes) : [...props.defaultExpandedKeys]);
|
|
149
137
|
watch(() => props.defaultExpandedKeys, (next) => {
|
|
@@ -221,7 +209,7 @@ const SelectTree = /* @__PURE__ */ defineComponent({
|
|
|
221
209
|
}, null), createVNode(SelectPanel, {
|
|
222
210
|
"id": panelId,
|
|
223
211
|
"elementRef": panelRef,
|
|
224
|
-
"searchable":
|
|
212
|
+
"searchable": props.searchable,
|
|
225
213
|
"keyword": keyword.value,
|
|
226
214
|
"onKeyword": (value) => {
|
|
227
215
|
keyword.value = value;
|
|
@@ -35,11 +35,12 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
|
|
|
35
35
|
type: BooleanConstructor;
|
|
36
36
|
default: boolean;
|
|
37
37
|
};
|
|
38
|
+
/** 无值时的占位文案(**透传给输入面**:`hasValue === false` 时由它渲染并取代 `default` 插槽) */
|
|
38
39
|
placeholder: {
|
|
39
40
|
type: StringConstructor;
|
|
40
41
|
default: string;
|
|
41
42
|
};
|
|
42
|
-
/**
|
|
43
|
+
/** 单选显示文本(只在**有值**时渲染;无值态由输入面的占位承担) */
|
|
43
44
|
label: {
|
|
44
45
|
type: StringConstructor;
|
|
45
46
|
default: undefined;
|
|
@@ -93,11 +94,12 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
|
|
|
93
94
|
type: BooleanConstructor;
|
|
94
95
|
default: boolean;
|
|
95
96
|
};
|
|
97
|
+
/** 无值时的占位文案(**透传给输入面**:`hasValue === false` 时由它渲染并取代 `default` 插槽) */
|
|
96
98
|
placeholder: {
|
|
97
99
|
type: StringConstructor;
|
|
98
100
|
default: string;
|
|
99
101
|
};
|
|
100
|
-
/**
|
|
102
|
+
/** 单选显示文本(只在**有值**时渲染;无值态由输入面的占位承担) */
|
|
101
103
|
label: {
|
|
102
104
|
type: StringConstructor;
|
|
103
105
|
default: undefined;
|
|
@@ -38,11 +38,12 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
|
|
|
38
38
|
type: Boolean,
|
|
39
39
|
default: false
|
|
40
40
|
},
|
|
41
|
+
/** 无值时的占位文案(**透传给输入面**:`hasValue === false` 时由它渲染并取代 `default` 插槽) */
|
|
41
42
|
placeholder: {
|
|
42
43
|
type: String,
|
|
43
44
|
default: "请选择"
|
|
44
45
|
},
|
|
45
|
-
/**
|
|
46
|
+
/** 单选显示文本(只在**有值**时渲染;无值态由输入面的占位承担) */
|
|
46
47
|
label: {
|
|
47
48
|
type: String,
|
|
48
49
|
default: void 0
|
|
@@ -72,6 +73,7 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
|
|
|
72
73
|
"disabled": props.disabled,
|
|
73
74
|
"clearable": props.clearable,
|
|
74
75
|
"hasValue": props.hasValue,
|
|
76
|
+
"placeholder": props.placeholder,
|
|
75
77
|
"class": "mh-popup-anchor mh-select-host",
|
|
76
78
|
"controlClass": controlClass(),
|
|
77
79
|
"controlAttrs": {
|
|
@@ -84,9 +86,12 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
|
|
|
84
86
|
},
|
|
85
87
|
"onChange": () => props.onClear?.()
|
|
86
88
|
}, {
|
|
87
|
-
default: () => props.renderValue ? props.renderValue() :
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
default: () => props.renderValue ? props.renderValue() : (
|
|
90
|
+
/* 只画**有值**时的那一行;无值态在上面被输入面的占位取代(`default` 不会被调用) */
|
|
91
|
+
createVNode("span", {
|
|
92
|
+
"class": "mh-select__value"
|
|
93
|
+
}, [props.label])
|
|
94
|
+
),
|
|
90
95
|
/* 箭头沿用输入框的右侧图标位(槽内尺寸收敛见 `select.css` 的 `.mh-select-host` 一节);
|
|
91
96
|
展开转向由 `popup.css` 的 `.mh-popup-caret` 承担(含触发器进表壳后的兄弟档) */
|
|
92
97
|
suffix: () => createVNode(Icon, {
|