niuma-ui 1.2.5 → 1.2.8
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/CHANGELOG.md +27 -0
- package/dist/components/RsAutoComplete.d.ts +39 -0
- package/dist/components/RsAutoComplete.impl.js +306 -0
- package/dist/components/RsAutoComplete.js +5 -0
- package/dist/components/RsCascader.css +81 -0
- package/dist/components/RsCascader.d.ts +34 -0
- package/dist/components/RsCascader.impl.js +138 -0
- package/dist/components/RsCascader.js +8 -0
- package/dist/components/RsMentions-1.css +29 -0
- package/dist/components/RsMentions.css +19 -0
- package/dist/components/RsMentions.d.ts +32 -0
- package/dist/components/RsMentions.impl.js +242 -0
- package/dist/components/RsMentions.js +10 -0
- package/dist/components/RsSelect.css +51 -36
- package/dist/components/RsSelect.d.ts +27 -7
- package/dist/components/RsSelect.impl.js +145 -30
- package/dist/components/RsSelect.js +1 -1
- package/dist/components/RsSplitPane.impl.js +6 -2
- package/dist/components/RsTabs.d.ts +1 -1
- package/dist/components/RsToaster.d.ts +1 -1
- package/dist/components/RsTree.d.ts +4 -4
- package/dist/components/RsTree.impl.js +1 -1
- package/dist/components/RsTreeSelect.css +60 -0
- package/dist/components/RsTreeSelect.d.ts +33 -0
- package/dist/components/RsTreeSelect.impl.js +145 -0
- package/dist/components/RsTreeSelect.js +8 -0
- package/dist/components/cascader-utils.d.ts +16 -0
- package/dist/components/cascader-utils.js +39 -0
- package/dist/components/mentions-utils.d.ts +46 -0
- package/dist/components/mentions-utils.js +128 -0
- package/dist/components/overlay-utils.d.ts +31 -0
- package/dist/components/overlay-utils.js +29 -1
- package/dist/components/select-utils.d.ts +15 -0
- package/dist/components/select-utils.js +37 -1
- package/dist/components/table/RsTableCellEditor.impl.js +7 -1
- package/dist/components/use-rs-select.d.ts +5 -2
- package/dist/components/use-rs-select.js +31 -19
- package/dist/composables/useRsTableShell.js +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +6 -0
- package/dist/locale/messages.js +6 -0
- package/dist/styles.css +160 -0
- package/dist/vite-plugins/niuma-ui-host.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useRsI18n } from "../composables/useRsI18n.js";
|
|
2
|
+
import RsIcon_default from "./RsIcon.js";
|
|
3
|
+
import { useResolvedRsComponentSize } from "./resolve-size.js";
|
|
4
|
+
import RsPopover_default from "./RsPopover.js";
|
|
5
|
+
import { buildTreeNodeIndex, getTreeLabel, resolveTreeFieldNames } from "./tree-utils.js";
|
|
6
|
+
import RsTree_default from "./RsTree.js";
|
|
7
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, mergeModels, normalizeClass, openBlock, ref, toDisplayString, unref, useModel, vModelText, withCtx, withDirectives, withModifiers } from "vue";
|
|
8
|
+
//#region src/components/RsTreeSelect.vue?vue&type=script&setup=true&lang.ts
|
|
9
|
+
var _hoisted_1 = ["disabled"];
|
|
10
|
+
var _hoisted_2 = {
|
|
11
|
+
key: 0,
|
|
12
|
+
class: "rs-tree-select__value"
|
|
13
|
+
};
|
|
14
|
+
var _hoisted_3 = {
|
|
15
|
+
key: 1,
|
|
16
|
+
class: "rs-tree-select__placeholder"
|
|
17
|
+
};
|
|
18
|
+
var _hoisted_4 = ["aria-label"];
|
|
19
|
+
var _hoisted_5 = ["placeholder"];
|
|
20
|
+
var RsTreeSelect_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
21
|
+
name: "RsTreeSelect",
|
|
22
|
+
__name: "RsTreeSelect",
|
|
23
|
+
props: /*@__PURE__*/ mergeModels({
|
|
24
|
+
treeData: {},
|
|
25
|
+
fieldNames: {},
|
|
26
|
+
placeholder: {},
|
|
27
|
+
disabled: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: false
|
|
30
|
+
},
|
|
31
|
+
allowClear: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: false
|
|
34
|
+
},
|
|
35
|
+
multiple: {
|
|
36
|
+
type: Boolean,
|
|
37
|
+
default: false
|
|
38
|
+
},
|
|
39
|
+
checkable: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: false
|
|
42
|
+
},
|
|
43
|
+
searchable: {
|
|
44
|
+
type: Boolean,
|
|
45
|
+
default: false
|
|
46
|
+
},
|
|
47
|
+
size: {}
|
|
48
|
+
}, {
|
|
49
|
+
"modelValue": { default: "" },
|
|
50
|
+
"modelModifiers": {},
|
|
51
|
+
"open": {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false
|
|
54
|
+
},
|
|
55
|
+
"openModifiers": {}
|
|
56
|
+
}),
|
|
57
|
+
emits: ["update:modelValue", "update:open"],
|
|
58
|
+
setup(__props) {
|
|
59
|
+
const model = useModel(__props, "modelValue");
|
|
60
|
+
const open = useModel(__props, "open");
|
|
61
|
+
const props = __props;
|
|
62
|
+
const { t } = useRsI18n();
|
|
63
|
+
const resolvedSize = useResolvedRsComponentSize(() => props.size);
|
|
64
|
+
const filter = ref("");
|
|
65
|
+
const names = computed(() => resolveTreeFieldNames(props.fieldNames));
|
|
66
|
+
const index = computed(() => buildTreeNodeIndex(props.treeData, names.value));
|
|
67
|
+
const selectedKeys = computed(() => {
|
|
68
|
+
if (Array.isArray(model.value)) return model.value.map(String);
|
|
69
|
+
return model.value === "" || model.value == null ? [] : [String(model.value)];
|
|
70
|
+
});
|
|
71
|
+
const display = computed(() => {
|
|
72
|
+
return selectedKeys.value.map((key) => {
|
|
73
|
+
const entry = index.value.get(key);
|
|
74
|
+
return entry ? getTreeLabel(entry.node, names.value) : key;
|
|
75
|
+
}).join(", ");
|
|
76
|
+
});
|
|
77
|
+
function onTreeUpdate(value) {
|
|
78
|
+
model.value = props.multiple || props.checkable ? value : value;
|
|
79
|
+
if (!props.multiple && !props.checkable) open.value = false;
|
|
80
|
+
}
|
|
81
|
+
function onClear() {
|
|
82
|
+
model.value = props.multiple || props.checkable ? [] : "";
|
|
83
|
+
}
|
|
84
|
+
return (_ctx, _cache) => {
|
|
85
|
+
return openBlock(), createBlock(RsPopover_default, {
|
|
86
|
+
open: open.value,
|
|
87
|
+
"onUpdate:open": _cache[2] || (_cache[2] = ($event) => open.value = $event),
|
|
88
|
+
side: "bottom",
|
|
89
|
+
align: "start",
|
|
90
|
+
width: "md"
|
|
91
|
+
}, {
|
|
92
|
+
content: withCtx(() => [createElementVNode("div", {
|
|
93
|
+
class: "rs-tree-select__panel",
|
|
94
|
+
onMousedown: _cache[1] || (_cache[1] = withModifiers(() => {}, ["prevent"]))
|
|
95
|
+
}, [__props.searchable ? withDirectives((openBlock(), createElementBlock("input", {
|
|
96
|
+
key: 0,
|
|
97
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => filter.value = $event),
|
|
98
|
+
class: "rs-tree-select__search",
|
|
99
|
+
placeholder: unref(t)("select.searchPlaceholder")
|
|
100
|
+
}, null, 8, _hoisted_5)), [[vModelText, filter.value]]) : createCommentVNode("", true), createVNode(RsTree_default, {
|
|
101
|
+
"model-value": __props.multiple || __props.checkable ? selectedKeys.value : selectedKeys.value[0] ?? "",
|
|
102
|
+
nodes: __props.treeData,
|
|
103
|
+
"field-names": __props.fieldNames,
|
|
104
|
+
multiple: __props.multiple,
|
|
105
|
+
checkable: __props.checkable,
|
|
106
|
+
filter: filter.value,
|
|
107
|
+
highlight: "",
|
|
108
|
+
"default-expand-all": "",
|
|
109
|
+
"onUpdate:modelValue": onTreeUpdate
|
|
110
|
+
}, null, 8, [
|
|
111
|
+
"model-value",
|
|
112
|
+
"nodes",
|
|
113
|
+
"field-names",
|
|
114
|
+
"multiple",
|
|
115
|
+
"checkable",
|
|
116
|
+
"filter"
|
|
117
|
+
])], 32)]),
|
|
118
|
+
default: withCtx(() => [createElementVNode("button", {
|
|
119
|
+
type: "button",
|
|
120
|
+
class: normalizeClass(["rs-tree-select", [`rs-tree-select--${unref(resolvedSize)}`, { "rs-tree-select--disabled": __props.disabled }]]),
|
|
121
|
+
disabled: __props.disabled
|
|
122
|
+
}, [
|
|
123
|
+
display.value ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(display.value), 1)) : (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(__props.placeholder ?? unref(t)("select.placeholder")), 1)),
|
|
124
|
+
__props.allowClear && display.value && !__props.disabled ? (openBlock(), createElementBlock("button", {
|
|
125
|
+
key: 2,
|
|
126
|
+
type: "button",
|
|
127
|
+
class: "rs-tree-select__clear",
|
|
128
|
+
"aria-label": unref(t)("select.clear"),
|
|
129
|
+
onClick: withModifiers(onClear, ["stop"])
|
|
130
|
+
}, [createVNode(RsIcon_default, {
|
|
131
|
+
name: "x",
|
|
132
|
+
size: 12
|
|
133
|
+
})], 8, _hoisted_4)) : createCommentVNode("", true),
|
|
134
|
+
createVNode(RsIcon_default, {
|
|
135
|
+
name: "chevron-down",
|
|
136
|
+
size: 14
|
|
137
|
+
})
|
|
138
|
+
], 10, _hoisted_1)]),
|
|
139
|
+
_: 1
|
|
140
|
+
}, 8, ["open"]);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
//#endregion
|
|
145
|
+
export { RsTreeSelect_vue_vue_type_script_setup_true_lang_default as default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import './RsTreeSelect.css'
|
|
2
|
+
import _plugin_vue_export_helper_default from "../_virtual/_plugin-vue_export-helper.js";
|
|
3
|
+
import RsTreeSelect_vue_vue_type_script_setup_true_lang_default from "./RsTreeSelect.impl.js";
|
|
4
|
+
|
|
5
|
+
//#region src/components/RsTreeSelect.vue
|
|
6
|
+
var RsTreeSelect_default = /*#__PURE__*/ _plugin_vue_export_helper_default(RsTreeSelect_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-a057dd37"]]);
|
|
7
|
+
//#endregion
|
|
8
|
+
export { RsTreeSelect_default as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RsSelectValue } from './select-utils';
|
|
2
|
+
/** 级联选项,对齐 Ant Cascader options */
|
|
3
|
+
export interface RsCascaderOption {
|
|
4
|
+
label: string;
|
|
5
|
+
value: RsSelectValue;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
children?: RsCascaderOption[];
|
|
8
|
+
isLeaf?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type RsCascaderPath = RsSelectValue[];
|
|
11
|
+
export type RsCascaderExpandTrigger = 'click' | 'hover';
|
|
12
|
+
export declare function findCascaderOption(options: readonly RsCascaderOption[], value: RsSelectValue): RsCascaderOption | undefined;
|
|
13
|
+
export declare function cascaderColumns(options: readonly RsCascaderOption[], path: RsCascaderPath): RsCascaderOption[][];
|
|
14
|
+
export declare function cascaderLabels(options: readonly RsCascaderOption[], path: RsCascaderPath): string[];
|
|
15
|
+
export declare function cascaderDisplay(options: readonly RsCascaderOption[], path: RsCascaderPath, separator?: string): string;
|
|
16
|
+
export declare function isCascaderLeaf(option: RsCascaderOption): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/components/cascader-utils.ts
|
|
2
|
+
function findCascaderOption(options, value) {
|
|
3
|
+
return options.find((item) => String(item.value) === String(value));
|
|
4
|
+
}
|
|
5
|
+
function cascaderColumns(options, path) {
|
|
6
|
+
const columns = [options.slice()];
|
|
7
|
+
let level = options;
|
|
8
|
+
for (const token of path) {
|
|
9
|
+
const children = findCascaderOption(level, token)?.children;
|
|
10
|
+
if (!children?.length) break;
|
|
11
|
+
columns.push(children);
|
|
12
|
+
level = children;
|
|
13
|
+
}
|
|
14
|
+
return columns;
|
|
15
|
+
}
|
|
16
|
+
function cascaderLabels(options, path) {
|
|
17
|
+
const labels = [];
|
|
18
|
+
let level = options;
|
|
19
|
+
for (const token of path) {
|
|
20
|
+
const current = findCascaderOption(level, token);
|
|
21
|
+
if (!current) {
|
|
22
|
+
labels.push(String(token));
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
labels.push(current.label);
|
|
26
|
+
level = current.children ?? [];
|
|
27
|
+
}
|
|
28
|
+
return labels;
|
|
29
|
+
}
|
|
30
|
+
function cascaderDisplay(options, path, separator = " / ") {
|
|
31
|
+
return cascaderLabels(options, path).join(separator);
|
|
32
|
+
}
|
|
33
|
+
function isCascaderLeaf(option) {
|
|
34
|
+
if (option.isLeaf === true) return true;
|
|
35
|
+
if (option.isLeaf === false) return false;
|
|
36
|
+
return !option.children?.length;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { cascaderColumns, cascaderDisplay, cascaderLabels, findCascaderOption, isCascaderLeaf };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface RsMentionOption {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface RsMentionActive {
|
|
7
|
+
prefix: string;
|
|
8
|
+
query: string;
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveMentionPrefixes(prefix: string | readonly string[]): string[];
|
|
13
|
+
/** 光标前最近一个前缀 + 查询词(不含空格) */
|
|
14
|
+
export declare function findActiveMention(text: string, cursor: number, prefixes: readonly string[], split?: string): RsMentionActive | null;
|
|
15
|
+
export declare function applyMention(text: string, active: RsMentionActive, value: string, split?: string): {
|
|
16
|
+
text: string;
|
|
17
|
+
cursor: number;
|
|
18
|
+
};
|
|
19
|
+
export interface RsMentionCaretBox {
|
|
20
|
+
top: number;
|
|
21
|
+
left: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
export interface RsMentionPopupBox {
|
|
25
|
+
top: number;
|
|
26
|
+
left: number;
|
|
27
|
+
placement: 'top' | 'bottom';
|
|
28
|
+
}
|
|
29
|
+
export declare function stepMentionIndex(options: readonly RsMentionOption[], current: number, delta: 1 | -1): number;
|
|
30
|
+
/** 视口内避让:下边不够就翻到上方,左右夹进窗口。 */
|
|
31
|
+
export declare function placeMentionPopup(caret: RsMentionCaretBox, popup: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}, viewport: {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}, gap?: number): RsMentionPopupBox;
|
|
38
|
+
export interface RsTextareaCaretMeter {
|
|
39
|
+
measure: (textarea: HTMLTextAreaElement, index: number) => RsMentionCaretBox;
|
|
40
|
+
dispose: () => void;
|
|
41
|
+
}
|
|
42
|
+
/** 复用隐藏镜像节点,避免每次按键 insert/remove 逼 reflow。 */
|
|
43
|
+
export declare function createTextareaCaretMeter(): RsTextareaCaretMeter;
|
|
44
|
+
/** 相对 textarea 内容区的光标坐标(已扣滚动)。测试 / 单次调用用;热路径请复用 meter。 */
|
|
45
|
+
export declare function measureTextareaCaret(textarea: HTMLTextAreaElement, index: number): RsMentionCaretBox;
|
|
46
|
+
export declare function filterMentionOptions(options: readonly RsMentionOption[], query: string): RsMentionOption[];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { placeAnchoredPopup, stepEnabledIndex } from "./overlay-utils.js";
|
|
2
|
+
//#region src/components/mentions-utils.ts
|
|
3
|
+
function resolveMentionPrefixes(prefix) {
|
|
4
|
+
return (Array.isArray(prefix) ? prefix : [prefix]).map(String).filter(Boolean);
|
|
5
|
+
}
|
|
6
|
+
/** 光标前最近一个前缀 + 查询词(不含空格) */
|
|
7
|
+
function findActiveMention(text, cursor, prefixes, split = " ") {
|
|
8
|
+
const head = text.slice(0, Math.max(0, cursor));
|
|
9
|
+
let found = null;
|
|
10
|
+
for (const token of prefixes) {
|
|
11
|
+
const at = head.lastIndexOf(token);
|
|
12
|
+
if (at < 0) continue;
|
|
13
|
+
const after = head.slice(at + token.length);
|
|
14
|
+
if (after.includes(split) || after.includes("\n")) continue;
|
|
15
|
+
if (!found || at > found.start) found = {
|
|
16
|
+
prefix: token,
|
|
17
|
+
query: after,
|
|
18
|
+
start: at,
|
|
19
|
+
end: cursor
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
function applyMention(text, active, value, split = " ") {
|
|
25
|
+
const insert = `${active.prefix}${value}${split}`;
|
|
26
|
+
return {
|
|
27
|
+
text: `${text.slice(0, active.start)}${insert}${text.slice(active.end)}`,
|
|
28
|
+
cursor: active.start + insert.length
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function stepMentionIndex(options, current, delta) {
|
|
32
|
+
return stepEnabledIndex(options, current, delta);
|
|
33
|
+
}
|
|
34
|
+
/** 视口内避让:下边不够就翻到上方,左右夹进窗口。 */
|
|
35
|
+
function placeMentionPopup(caret, popup, viewport, gap = 4) {
|
|
36
|
+
const box = placeAnchoredPopup(caret, popup, viewport, gap);
|
|
37
|
+
return {
|
|
38
|
+
top: box.top,
|
|
39
|
+
left: box.left,
|
|
40
|
+
placement: box.placement
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
var MIRROR_STYLE_KEYS = [
|
|
44
|
+
"boxSizing",
|
|
45
|
+
"width",
|
|
46
|
+
"height",
|
|
47
|
+
"overflowX",
|
|
48
|
+
"overflowY",
|
|
49
|
+
"borderTopWidth",
|
|
50
|
+
"borderRightWidth",
|
|
51
|
+
"borderBottomWidth",
|
|
52
|
+
"borderLeftWidth",
|
|
53
|
+
"paddingTop",
|
|
54
|
+
"paddingRight",
|
|
55
|
+
"paddingBottom",
|
|
56
|
+
"paddingLeft",
|
|
57
|
+
"fontStyle",
|
|
58
|
+
"fontVariant",
|
|
59
|
+
"fontWeight",
|
|
60
|
+
"fontStretch",
|
|
61
|
+
"fontSize",
|
|
62
|
+
"fontFamily",
|
|
63
|
+
"letterSpacing",
|
|
64
|
+
"textIndent",
|
|
65
|
+
"textTransform",
|
|
66
|
+
"wordSpacing",
|
|
67
|
+
"tabSize",
|
|
68
|
+
"lineHeight",
|
|
69
|
+
"whiteSpace",
|
|
70
|
+
"wordWrap"
|
|
71
|
+
];
|
|
72
|
+
/** 复用隐藏镜像节点,避免每次按键 insert/remove 逼 reflow。 */
|
|
73
|
+
function createTextareaCaretMeter() {
|
|
74
|
+
let mirror = null;
|
|
75
|
+
let marker = null;
|
|
76
|
+
function ensure() {
|
|
77
|
+
if (mirror && marker) return {
|
|
78
|
+
mirror,
|
|
79
|
+
marker
|
|
80
|
+
};
|
|
81
|
+
mirror = document.createElement("div");
|
|
82
|
+
mirror.setAttribute("aria-hidden", "true");
|
|
83
|
+
const s = mirror.style;
|
|
84
|
+
s.position = "absolute";
|
|
85
|
+
s.visibility = "hidden";
|
|
86
|
+
s.pointerEvents = "none";
|
|
87
|
+
s.whiteSpace = "pre-wrap";
|
|
88
|
+
s.wordWrap = "break-word";
|
|
89
|
+
s.top = "0";
|
|
90
|
+
s.left = "-9999px";
|
|
91
|
+
marker = document.createElement("span");
|
|
92
|
+
marker.textContent = ".";
|
|
93
|
+
document.body.appendChild(mirror);
|
|
94
|
+
return {
|
|
95
|
+
mirror,
|
|
96
|
+
marker
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function measure(textarea, index) {
|
|
100
|
+
const { mirror: box, marker: caret } = ensure();
|
|
101
|
+
const style = window.getComputedStyle(textarea);
|
|
102
|
+
for (const key of MIRROR_STYLE_KEYS) box.style.setProperty(key.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`), style[key]);
|
|
103
|
+
const text = textarea.value.slice(0, Math.max(0, index));
|
|
104
|
+
box.textContent = text.endsWith("\n") ? `${text}\u00a0` : text;
|
|
105
|
+
box.appendChild(caret);
|
|
106
|
+
return {
|
|
107
|
+
top: caret.offsetTop - textarea.scrollTop,
|
|
108
|
+
left: caret.offsetLeft - textarea.scrollLeft,
|
|
109
|
+
height: caret.offsetHeight || parseFloat(style.lineHeight) || 16
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function dispose() {
|
|
113
|
+
mirror?.remove();
|
|
114
|
+
mirror = null;
|
|
115
|
+
marker = null;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
measure,
|
|
119
|
+
dispose
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function filterMentionOptions(options, query) {
|
|
123
|
+
const q = query.trim().toLowerCase();
|
|
124
|
+
if (!q) return options.filter((item) => !item.disabled);
|
|
125
|
+
return options.filter((item) => !item.disabled && (item.label.toLowerCase().includes(q) || item.value.toLowerCase().includes(q)));
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { applyMention, createTextareaCaretMeter, filterMentionOptions, findActiveMention, placeMentionPopup, resolveMentionPrefixes, stepMentionIndex };
|
|
@@ -21,3 +21,34 @@ export declare const RS_TOAST_DEFAULT_POSITION: RsToastPosition;
|
|
|
21
21
|
export declare const RS_TOAST_DEFAULT_GAP = 4;
|
|
22
22
|
export declare const rsToastPositions: readonly ["top-center", "top-left", "top-right", "bottom-center", "bottom-left", "bottom-right"];
|
|
23
23
|
export declare function rsFeedbackIconClass(tone: RsFeedbackTone): string;
|
|
24
|
+
/** 视口内锚点矩形(输入框、插入符等)。 */
|
|
25
|
+
export interface RsOverlayAnchorBox {
|
|
26
|
+
top: number;
|
|
27
|
+
left: number;
|
|
28
|
+
height: number;
|
|
29
|
+
width?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface RsOverlayPopupSize {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}
|
|
35
|
+
export interface RsOverlayViewport {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
}
|
|
39
|
+
/** 锚点浮层最终盒子。placement 供翻转后的样式钩子使用。 */
|
|
40
|
+
export interface RsOverlayBox {
|
|
41
|
+
top: number;
|
|
42
|
+
left: number;
|
|
43
|
+
width: number;
|
|
44
|
+
placement: 'top' | 'bottom';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 把浮层贴到锚点:优先下方,视口不够则翻到上方,左右夹进窗口。
|
|
48
|
+
* 对齐 WAI-ARIA APG 浮层惯例与 CSS overflow 避让,不跟某一家组件库的 API。
|
|
49
|
+
*/
|
|
50
|
+
export declare function placeAnchoredPopup(anchor: RsOverlayAnchorBox, popup: RsOverlayPopupSize, viewport: RsOverlayViewport, gap?: number): RsOverlayBox;
|
|
51
|
+
/** 在可选项里按方向跳过 disabled,供 combobox / listbox 方向键与 Home / End。 */
|
|
52
|
+
export declare function stepEnabledIndex<T extends {
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
}>(options: readonly T[], current: number, delta: 1 | -1): number;
|
|
@@ -13,5 +13,33 @@ var rsToastPositions = [
|
|
|
13
13
|
function rsFeedbackIconClass(tone) {
|
|
14
14
|
return `rs-feedback-icon rs-feedback-icon--${tone}`;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* 把浮层贴到锚点:优先下方,视口不够则翻到上方,左右夹进窗口。
|
|
18
|
+
* 对齐 WAI-ARIA APG 浮层惯例与 CSS overflow 避让,不跟某一家组件库的 API。
|
|
19
|
+
*/
|
|
20
|
+
function placeAnchoredPopup(anchor, popup, viewport, gap = 4) {
|
|
21
|
+
const below = anchor.top + anchor.height + gap;
|
|
22
|
+
const above = anchor.top - popup.height - gap;
|
|
23
|
+
const placement = below + popup.height <= viewport.height || above < gap ? "bottom" : "top";
|
|
24
|
+
const top = placement === "bottom" ? below : Math.max(gap, above);
|
|
25
|
+
const width = Math.min(popup.width, Math.max(0, viewport.width - gap * 2));
|
|
26
|
+
const maxLeft = Math.max(gap, viewport.width - width - gap);
|
|
27
|
+
return {
|
|
28
|
+
top,
|
|
29
|
+
left: Math.min(Math.max(gap, anchor.left), maxLeft),
|
|
30
|
+
width,
|
|
31
|
+
placement
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** 在可选项里按方向跳过 disabled,供 combobox / listbox 方向键与 Home / End。 */
|
|
35
|
+
function stepEnabledIndex(options, current, delta) {
|
|
36
|
+
if (!options.length) return 0;
|
|
37
|
+
let index = current;
|
|
38
|
+
for (let n = 0; n < options.length; n += 1) {
|
|
39
|
+
index = (index + delta + options.length) % options.length;
|
|
40
|
+
if (!options[index]?.disabled) return index;
|
|
41
|
+
}
|
|
42
|
+
return Math.max(0, current);
|
|
43
|
+
}
|
|
16
44
|
//#endregion
|
|
17
|
-
export { RS_TOAST_DEFAULT_GAP, RS_TOAST_DEFAULT_POSITION, rsFeedbackIconClass, rsToastPositions };
|
|
45
|
+
export { RS_TOAST_DEFAULT_GAP, RS_TOAST_DEFAULT_POSITION, placeAnchoredPopup, rsFeedbackIconClass, rsToastPositions, stepEnabledIndex };
|
|
@@ -3,6 +3,14 @@ export type RsSelectValue = string | number;
|
|
|
3
3
|
export type RsSelectOptionFilterProp = string;
|
|
4
4
|
export type RsSelectPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
5
5
|
export type RsSelectStatus = 'error' | 'warning' | '';
|
|
6
|
+
/** 对齐 Ant Design 5 Select variant;搜索仍在面板内,不改触发器打字。 */
|
|
7
|
+
export type RsSelectVariant = 'outlined' | 'filled' | 'borderless';
|
|
8
|
+
/** 多选折叠:数字为上限;responsive 按触发器宽度收。 */
|
|
9
|
+
export type RsSelectMaxTagCount = number | 'responsive';
|
|
10
|
+
export declare function splitSelectLabelHighlight(label: string, keyword: string): Array<{
|
|
11
|
+
text: string;
|
|
12
|
+
highlight: boolean;
|
|
13
|
+
}>;
|
|
6
14
|
/** 自定义过滤(对齐 Ant Design filterOption)。返回 false 则隐藏该项。 */
|
|
7
15
|
export type RsSelectFilterOption = (query: string, option: RsSelectOption) => boolean;
|
|
8
16
|
/** 过滤后排序(对齐 Ant Design filterSort) */
|
|
@@ -41,6 +49,7 @@ export type RsSelectModelValue = RsSelectValue | RsSelectValue[] | RsSelectLabel
|
|
|
41
49
|
* 按泛型收窄后的 v-model。
|
|
42
50
|
* 默认单选、值为 string:宿主 `@update:model-value="(v: string) => void"` 可直接赋值。
|
|
43
51
|
* `multiple` / `labelInValue` 为字面量 true 时收成数组或 labeled;为 `boolean` 时保留联合。
|
|
52
|
+
* 组件本体的 defineModel 用 RsSelectModelValue(multiple / labelInValue 是运行时 boolean)。
|
|
44
53
|
*/
|
|
45
54
|
export type RsSelectResolvedModel<Value extends RsSelectValue = string, Multiple extends boolean = false, LabelInValue extends boolean = false> = LabelInValue extends true ? Multiple extends true ? RsSelectLabeledValue[] : Multiple extends false ? RsSelectLabeledValue | '' : RsSelectLabeledValue | RsSelectLabeledValue[] | '' : Multiple extends true ? Value[] : Multiple extends false ? Value | '' : Value | Value[] | '';
|
|
46
55
|
/**
|
|
@@ -48,6 +57,12 @@ export type RsSelectResolvedModel<Value extends RsSelectValue = string, Multiple
|
|
|
48
57
|
* 选项若传入 value: '',对内映射为此哨兵,避免崩溃;对外读写仍为 ''。
|
|
49
58
|
*/
|
|
50
59
|
export declare const RS_SELECT_EMPTY_VALUE = "__rs_select_empty__";
|
|
60
|
+
/**
|
|
61
|
+
* Vue 对泛型 boolean prop 不会按 Boolean 收口。
|
|
62
|
+
* 模板写 `multiple` 时运行时可能是 '';个别版本还会落到 attrs 变成 'true' / 'multiple'。
|
|
63
|
+
* Boolean('') 为 false,会把多选打成单选。
|
|
64
|
+
*/
|
|
65
|
+
export declare function isSelectMultiple(value: unknown): boolean;
|
|
51
66
|
/** 业务 value → ComboboxItem token(空串走哨兵;数字转为十进制字符串) */
|
|
52
67
|
export declare function toComboboxValue(value: RsSelectValue): string;
|
|
53
68
|
/** ComboboxItem token → 业务字符串(仅还原空串哨兵,不恢复 number) */
|
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
//#region src/components/select-utils.ts
|
|
2
|
+
function splitSelectLabelHighlight(label, keyword) {
|
|
3
|
+
const query = keyword.trim();
|
|
4
|
+
if (!query) return [{
|
|
5
|
+
text: label,
|
|
6
|
+
highlight: false
|
|
7
|
+
}];
|
|
8
|
+
const lowerLabel = label.toLowerCase();
|
|
9
|
+
const lowerQuery = query.toLowerCase();
|
|
10
|
+
const index = lowerLabel.indexOf(lowerQuery);
|
|
11
|
+
if (index < 0) return [{
|
|
12
|
+
text: label,
|
|
13
|
+
highlight: false
|
|
14
|
+
}];
|
|
15
|
+
const parts = [];
|
|
16
|
+
if (index > 0) parts.push({
|
|
17
|
+
text: label.slice(0, index),
|
|
18
|
+
highlight: false
|
|
19
|
+
});
|
|
20
|
+
parts.push({
|
|
21
|
+
text: label.slice(index, index + query.length),
|
|
22
|
+
highlight: true
|
|
23
|
+
});
|
|
24
|
+
if (index + query.length < label.length) parts.push({
|
|
25
|
+
text: label.slice(index + query.length),
|
|
26
|
+
highlight: false
|
|
27
|
+
});
|
|
28
|
+
return parts;
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
31
|
* Reka ComboboxItem 禁止 value 为空串(空串表示未选中 / placeholder)。
|
|
4
32
|
* 选项若传入 value: '',对内映射为此哨兵,避免崩溃;对外读写仍为 ''。
|
|
5
33
|
*/
|
|
6
34
|
var RS_SELECT_EMPTY_VALUE = "__rs_select_empty__";
|
|
35
|
+
/**
|
|
36
|
+
* Vue 对泛型 boolean prop 不会按 Boolean 收口。
|
|
37
|
+
* 模板写 `multiple` 时运行时可能是 '';个别版本还会落到 attrs 变成 'true' / 'multiple'。
|
|
38
|
+
* Boolean('') 为 false,会把多选打成单选。
|
|
39
|
+
*/
|
|
40
|
+
function isSelectMultiple(value) {
|
|
41
|
+
return value === true || value === "" || value === "true" || value === "multiple";
|
|
42
|
+
}
|
|
7
43
|
/** 业务 value → ComboboxItem token(空串走哨兵;数字转为十进制字符串) */
|
|
8
44
|
function toComboboxValue(value) {
|
|
9
45
|
return value === "" ? RS_SELECT_EMPTY_VALUE : String(value);
|
|
@@ -208,4 +244,4 @@ function splitByTokenSeparators(raw, separators) {
|
|
|
208
244
|
return raw.split(found);
|
|
209
245
|
}
|
|
210
246
|
//#endregion
|
|
211
|
-
export { RS_SELECT_EMPTY_VALUE, buildOptionDisabledMap, buildOptionLabelMap, comboboxBindingToTokens, filterSelectOptions, findSelectOption, flattenSelectOptions, flattenSelectValues, fromComboboxValue, isSelectLabeledValue, isSelectOptionGroup, normalizeSelectOptions, optionDisplayLabel, packSelectModel, restoreSelectValue, restoreSelectValues, selectModelEntries, sortSelectOptions, splitByTokenSeparators, toComboboxValue, toLabeledValue, toSelectedTokens, unwrapSelectEntry };
|
|
247
|
+
export { RS_SELECT_EMPTY_VALUE, buildOptionDisabledMap, buildOptionLabelMap, comboboxBindingToTokens, filterSelectOptions, findSelectOption, flattenSelectOptions, flattenSelectValues, fromComboboxValue, isSelectLabeledValue, isSelectMultiple, isSelectOptionGroup, normalizeSelectOptions, optionDisplayLabel, packSelectModel, restoreSelectValue, restoreSelectValues, selectModelEntries, sortSelectOptions, splitByTokenSeparators, splitSelectLabelHighlight, toComboboxValue, toLabeledValue, toSelectedTokens, unwrapSelectEntry };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRsI18n } from "../../composables/useRsI18n.js";
|
|
2
2
|
import RsInput_default from "../RsInput.js";
|
|
3
3
|
import RsInputNumber_default from "../RsInputNumber.js";
|
|
4
|
+
import { unwrapSelectEntry } from "../select-utils.js";
|
|
4
5
|
import RsSelect_default from "../RsSelect.js";
|
|
5
6
|
import RsDatePicker_default from "../RsDatePicker.js";
|
|
6
7
|
import { applyFocusMode, isNullDraft, nullToEditText, resolveCellEditorInputType, usesOverlayEditor } from "./table-edit-utils.js";
|
|
@@ -194,8 +195,13 @@ var RsTableCellEditor_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
|
|
|
194
195
|
emit("commit");
|
|
195
196
|
}
|
|
196
197
|
}
|
|
198
|
+
function selectDraftTokens(value) {
|
|
199
|
+
if (Array.isArray(value)) return value.map((item) => unwrapSelectEntry(item)).filter((item) => item !== null).map(String);
|
|
200
|
+
const raw = unwrapSelectEntry(value);
|
|
201
|
+
return raw == null ? "" : String(raw);
|
|
202
|
+
}
|
|
197
203
|
function onSelectUpdate(value) {
|
|
198
|
-
const tokens =
|
|
204
|
+
const tokens = selectDraftTokens(value);
|
|
199
205
|
if (tokens === "" || Array.isArray(tokens) && tokens.length === 0) {
|
|
200
206
|
if (!props.editorOptions?.clearable) return;
|
|
201
207
|
model.value = props.allowNull ? nullToEditText() : "";
|
|
@@ -15,7 +15,9 @@ export interface RsSelectEngineProps {
|
|
|
15
15
|
remote?: boolean;
|
|
16
16
|
virtual?: boolean;
|
|
17
17
|
virtualThreshold?: number;
|
|
18
|
-
maxTagCount?: number;
|
|
18
|
+
maxTagCount?: number | 'responsive';
|
|
19
|
+
/** 远程 @search 防抖毫秒;0 立即发 */
|
|
20
|
+
debounce?: number;
|
|
19
21
|
maxTagPlaceholder?: string | ((omitted: number) => string);
|
|
20
22
|
maxTagTooltip?: boolean;
|
|
21
23
|
maxTagTextLength?: number;
|
|
@@ -40,12 +42,13 @@ export type RsSelectEngineEmit = {
|
|
|
40
42
|
* Select 过滤、选中、多选 tag、creatable / 分隔符提交。
|
|
41
43
|
* 组件模板只消费返回值;表单校验仍留在 SFC。
|
|
42
44
|
*/
|
|
43
|
-
export declare function useRsSelect(props: RsSelectEngineProps, model: ModelRef<
|
|
45
|
+
export declare function useRsSelect<T extends RsSelectModelValue = RsSelectModelValue>(props: RsSelectEngineProps, model: ModelRef<T>, open: ModelRef<boolean>, searchQuery: ModelRef<string>, emit: RsSelectEngineEmit, t: RsTranslateFn): {
|
|
44
46
|
normalizedOptions: import("vue").ComputedRef<import("./select-utils").RsSelectOptions>;
|
|
45
47
|
resolvedPlaceholder: import("vue").ComputedRef<string>;
|
|
46
48
|
resolvedSearchPlaceholder: import("vue").ComputedRef<string>;
|
|
47
49
|
resolvedEmptyText: import("vue").ComputedRef<string>;
|
|
48
50
|
resolvedLoadingText: import("vue").ComputedRef<string>;
|
|
51
|
+
isMultiple: import("vue").ComputedRef<boolean>;
|
|
49
52
|
isSearchable: import("vue").ComputedRef<boolean>;
|
|
50
53
|
labelMap: import("vue").ComputedRef<Map<string, string>>;
|
|
51
54
|
useVirtual: import("vue").ComputedRef<boolean>;
|