@flux-ui/components 3.0.0-next.33 → 3.0.0-next.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component/FluxBoxedIcon.vue.d.ts +1 -5
- package/dist/component/FluxCommandPalette.vue.d.ts +52 -0
- package/dist/component/FluxCommandPaletteGroup.vue.d.ts +8 -0
- package/dist/component/FluxCommandPaletteItem.vue.d.ts +18 -0
- package/dist/component/FluxFormDateRangeInput.vue.d.ts +1 -5
- package/dist/component/FluxTag.vue.d.ts +1 -0
- package/dist/component/index.d.ts +4 -1
- package/dist/composable/private/useCommandPalette.d.ts +38 -0
- package/dist/index.css +320 -0
- package/dist/index.js +686 -120
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/component/FluxBoxedIcon.vue +0 -4
- package/src/component/FluxCalendar.vue +2 -1
- package/src/component/FluxChip.vue +1 -1
- package/src/component/FluxColorPicker.vue +1 -1
- package/src/component/FluxColorSelect.vue +3 -3
- package/src/component/FluxCommandPalette.vue +290 -0
- package/src/component/FluxCommandPaletteGroup.vue +23 -0
- package/src/component/FluxCommandPaletteItem.vue +60 -0
- package/src/component/FluxDatePicker.vue +0 -1
- package/src/component/FluxFader.vue +2 -2
- package/src/component/FluxFocalPointEditor.vue +3 -3
- package/src/component/FluxFormDateRangeInput.vue +0 -5
- package/src/component/FluxFormDateTimeInput.vue +2 -2
- package/src/component/FluxFormSelect.vue +2 -2
- package/src/component/FluxFormTextArea.vue +1 -0
- package/src/component/FluxQuantitySelector.vue +3 -6
- package/src/component/FluxSnackbar.vue +5 -3
- package/src/component/FluxTableActions.vue +3 -3
- package/src/component/FluxTag.vue +3 -1
- package/src/component/index.ts +4 -1
- package/src/component/primitive/CoordinatePicker.vue +10 -11
- package/src/component/primitive/FilterItem.vue +11 -2
- package/src/component/primitive/SelectBase.vue +2 -2
- package/src/component/primitive/SliderBase.vue +10 -13
- package/src/composable/private/useCommandPalette.ts +405 -0
- package/src/composable/private/useFormSelect.ts +2 -2
- package/src/css/component/Badge.module.scss +7 -0
- package/src/css/component/CommandPalette.module.scss +332 -0
- package/src/css/reset.scss +0 -107
- package/src/css/variables.scss +0 -26
- /package/dist/component/{FluxActions.vue.d.ts → FluxActionStack.vue.d.ts} +0 -0
- /package/src/component/{FluxActions.vue → FluxActionStack.vue} +0 -0
package/src/component/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { default as FluxAction } from './FluxAction.vue';
|
|
2
2
|
export { default as FluxActionBar } from './FluxActionBar.vue';
|
|
3
3
|
export { default as FluxActionPane } from './FluxActionPane.vue';
|
|
4
|
-
export { default as
|
|
4
|
+
export { default as FluxActionStack } from './FluxActionStack.vue';
|
|
5
5
|
export { default as FluxAnimatedColors } from './FluxAnimatedColors.vue';
|
|
6
6
|
export { default as FluxAspectRatio } from './FluxAspectRatio.vue';
|
|
7
7
|
export { default as FluxAutoGrid } from './FluxAutoGrid.vue';
|
|
@@ -18,6 +18,9 @@ export { default as FluxCalendarEvent } from './FluxCalendarEvent.vue';
|
|
|
18
18
|
export { default as FluxCheckbox } from './FluxCheckbox.vue';
|
|
19
19
|
export { default as FluxChip } from './FluxChip.vue';
|
|
20
20
|
export { default as FluxClickablePane } from './FluxClickablePane.vue';
|
|
21
|
+
export { default as FluxCommandPalette } from './FluxCommandPalette.vue';
|
|
22
|
+
export { default as FluxCommandPaletteGroup } from './FluxCommandPaletteGroup.vue';
|
|
23
|
+
export { default as FluxCommandPaletteItem } from './FluxCommandPaletteItem.vue';
|
|
21
24
|
export { default as FluxComment } from './FluxComment.vue';
|
|
22
25
|
export { default as FluxColorPicker } from './FluxColorPicker.vue';
|
|
23
26
|
export { default as FluxColorSelect } from './FluxColorSelect.vue';
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
setup>
|
|
20
20
|
import { roundStep } from '@basmilius/utils';
|
|
21
21
|
import { unrefTemplateElement } from '@flux-ui/internals';
|
|
22
|
-
import { computed,
|
|
22
|
+
import { computed, onUnmounted, ref, toRef, unref, useTemplateRef, watch } from 'vue';
|
|
23
23
|
import { useDisabled } from '$flux/composable';
|
|
24
24
|
import CoordinatePickerThumb from './CoordinatePickerThumb.vue';
|
|
25
25
|
import $style from '$flux/css/component/primitive/CoordinatePicker.module.scss';
|
|
@@ -58,16 +58,6 @@
|
|
|
58
58
|
(unref(modelValue)[1] - unref(min)[1]) / (unref(max)[1] - unref(min)[1])
|
|
59
59
|
]);
|
|
60
60
|
|
|
61
|
-
onMounted(() => {
|
|
62
|
-
document.addEventListener('pointermove', onPointerMove);
|
|
63
|
-
document.addEventListener('pointerup', onPointerUp, {passive: true});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
onUnmounted(() => {
|
|
67
|
-
document.removeEventListener('pointermove', onPointerMove);
|
|
68
|
-
document.removeEventListener('pointerup', onPointerUp);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
61
|
function onDecrement(x: boolean, y: boolean): void {
|
|
72
62
|
if (unref(disabled)) {
|
|
73
63
|
return;
|
|
@@ -116,6 +106,8 @@
|
|
|
116
106
|
}
|
|
117
107
|
|
|
118
108
|
isDragging.value = true;
|
|
109
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
110
|
+
document.addEventListener('pointerup', onPointerUp, {passive: true});
|
|
119
111
|
requestAnimationFrame(() => onPointerMove(evt));
|
|
120
112
|
}
|
|
121
113
|
|
|
@@ -149,7 +141,14 @@
|
|
|
149
141
|
|
|
150
142
|
function onPointerUp(): void {
|
|
151
143
|
isDragging.value = false;
|
|
144
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
145
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
watch(isDragging, isDragging => emit('dragging', isDragging));
|
|
149
|
+
|
|
150
|
+
onUnmounted(() => {
|
|
151
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
152
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
153
|
+
});
|
|
155
154
|
</script>
|
|
@@ -38,7 +38,16 @@
|
|
|
38
38
|
emit('click', evt);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
watch(() => item, async () => {
|
|
42
|
-
|
|
41
|
+
watch([() => item, () => value], async ([, nextValue], _prev, onCleanup) => {
|
|
42
|
+
let cancelled = false;
|
|
43
|
+
onCleanup(() => {
|
|
44
|
+
cancelled = true;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const nextLabel = await unref(getValueLabel)(nextValue);
|
|
48
|
+
|
|
49
|
+
if (!cancelled) {
|
|
50
|
+
valueLabel.value = nextLabel ?? undefined;
|
|
51
|
+
}
|
|
43
52
|
}, {deep: true, immediate: true});
|
|
44
53
|
</script>
|
|
@@ -286,8 +286,8 @@
|
|
|
286
286
|
return;
|
|
287
287
|
|
|
288
288
|
default:
|
|
289
|
-
if (evt.key.
|
|
290
|
-
highlightedIndex.value = unref(rawOptions).findIndex(o => o.label.toLowerCase().startsWith(evt.key));
|
|
289
|
+
if (evt.key.length === 1) {
|
|
290
|
+
highlightedIndex.value = unref(rawOptions).findIndex(o => o.label.toLowerCase().startsWith(evt.key.toLowerCase()));
|
|
291
291
|
} else {
|
|
292
292
|
highlightedIndex.value = -1;
|
|
293
293
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
setup>
|
|
23
23
|
import { unrefTemplateElement } from '@flux-ui/internals';
|
|
24
24
|
import { clsx } from 'clsx';
|
|
25
|
-
import {
|
|
25
|
+
import { onUnmounted, toRef, unref, useTemplateRef } from 'vue';
|
|
26
26
|
import { useDisabled } from '$flux/composable';
|
|
27
27
|
import FluxTicks from '$flux/component/FluxTicks.vue';
|
|
28
28
|
import $style from '$flux/css/component/primitive/Slider.module.scss';
|
|
@@ -47,22 +47,14 @@
|
|
|
47
47
|
const disabled = useDisabled(toRef(() => componentDisabled));
|
|
48
48
|
const rootRef = useTemplateRef('root');
|
|
49
49
|
|
|
50
|
-
onMounted(() => {
|
|
51
|
-
document.addEventListener('pointermove', onPointerMove);
|
|
52
|
-
document.addEventListener('pointerup', onPointerUp, {passive: true});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
onUnmounted(() => {
|
|
56
|
-
document.removeEventListener('pointermove', onPointerMove);
|
|
57
|
-
document.removeEventListener('pointerup', onPointerUp);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
50
|
function onPointerDown(evt: PointerEvent): void {
|
|
61
51
|
if (unref(disabled)) {
|
|
62
52
|
return;
|
|
63
53
|
}
|
|
64
54
|
|
|
65
55
|
emit('dragging', true);
|
|
56
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
57
|
+
document.addEventListener('pointerup', onPointerUp, {passive: true});
|
|
66
58
|
requestAnimationFrame(() => onPointerMove(evt));
|
|
67
59
|
}
|
|
68
60
|
|
|
@@ -77,13 +69,18 @@
|
|
|
77
69
|
left += 6; // margin.
|
|
78
70
|
width -= 12; // margin times two.
|
|
79
71
|
|
|
80
|
-
emit('update', Math.max(0, Math.min(1, (evt.
|
|
72
|
+
emit('update', Math.max(0, Math.min(1, (evt.clientX - left) / width)));
|
|
81
73
|
evt.preventDefault();
|
|
82
74
|
}
|
|
83
75
|
|
|
84
76
|
function onPointerUp(): void {
|
|
85
77
|
emit('dragging', false);
|
|
78
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
79
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
onUnmounted(() => {
|
|
83
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
84
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
85
|
+
});
|
|
89
86
|
</script>
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import type { FluxCommandSource, FluxCommandSourceItem, FluxCommandSubAction } from '@flux-ui/types';
|
|
2
|
+
import { useDebouncedRef } from '@basmilius/common';
|
|
3
|
+
import { computed, nextTick, type Ref, ref, unref, watch } from 'vue';
|
|
4
|
+
|
|
5
|
+
export type CommandPaletteResultItem = {
|
|
6
|
+
readonly globalIndex: number;
|
|
7
|
+
readonly sourceKey: string;
|
|
8
|
+
readonly sourceLabel: string;
|
|
9
|
+
readonly item: FluxCommandSourceItem;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CommandPaletteGroup = {
|
|
13
|
+
readonly sourceKey: string;
|
|
14
|
+
readonly sourceLabel: string;
|
|
15
|
+
readonly startIndex: number;
|
|
16
|
+
readonly items: CommandPaletteResultItem[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function useCommandPalette(params: {
|
|
20
|
+
readonly sources: Ref<FluxCommandSource[]>;
|
|
21
|
+
readonly itemRefs: Readonly<Ref<Array<{ readonly $el: HTMLElement; }> | null | undefined>>;
|
|
22
|
+
}): {
|
|
23
|
+
readonly search: Ref<string>;
|
|
24
|
+
readonly activeTab: Ref<string | null>;
|
|
25
|
+
readonly highlightedIndex: Ref<number>;
|
|
26
|
+
readonly isLoading: Ref<boolean>;
|
|
27
|
+
readonly isTransitioningBack: Ref<boolean>;
|
|
28
|
+
readonly subActionTarget: Ref<FluxCommandSourceItem | null>;
|
|
29
|
+
readonly filteredItems: Ref<CommandPaletteResultItem[]>;
|
|
30
|
+
readonly groupedItems: Ref<CommandPaletteGroup[]>;
|
|
31
|
+
readonly subActions: Ref<FluxCommandSubAction[]>;
|
|
32
|
+
readonly activeTabSource: Ref<FluxCommandSource | null>;
|
|
33
|
+
readonly tabs: Ref<FluxCommandSource[]>;
|
|
34
|
+
readonly totalItems: Ref<number>;
|
|
35
|
+
setSearch: (value: string) => void;
|
|
36
|
+
setActiveTab: (key: string | null) => void;
|
|
37
|
+
enterSubActions: (item: FluxCommandSourceItem) => void;
|
|
38
|
+
onKeyNavigate: (evt: KeyboardEvent, onClose: () => void, onActivate: (item: FluxCommandSourceItem) => void) => void;
|
|
39
|
+
reset: () => void;
|
|
40
|
+
} {
|
|
41
|
+
const search = ref('');
|
|
42
|
+
const activeTab = ref<string | null>(null);
|
|
43
|
+
const highlightedIndex = ref(-1);
|
|
44
|
+
const subActionTarget = ref<FluxCommandSourceItem | null>(null);
|
|
45
|
+
const isKeyboardNav = ref(false);
|
|
46
|
+
const isLoading = ref(false);
|
|
47
|
+
const isTransitioningBack = ref(false);
|
|
48
|
+
const savedState = ref<{ readonly search: string; readonly highlightedIndex: number; } | null>(null);
|
|
49
|
+
const asyncResults = ref<Map<string, FluxCommandSourceItem[]>>(new Map());
|
|
50
|
+
const debouncedSearch = useDebouncedRef(search, 300);
|
|
51
|
+
let fetchGeneration = 0;
|
|
52
|
+
|
|
53
|
+
const filteredItems = computed<CommandPaletteResultItem[]>(() => {
|
|
54
|
+
const query = unref(search).toLowerCase().trim();
|
|
55
|
+
const tab = unref(activeTab);
|
|
56
|
+
const sources = unref(params.sources);
|
|
57
|
+
const asyncMap = unref(asyncResults);
|
|
58
|
+
const results: CommandPaletteResultItem[] = [];
|
|
59
|
+
|
|
60
|
+
for (const source of sources) {
|
|
61
|
+
if (tab && source.key !== tab) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (source.fetchSearch) {
|
|
66
|
+
const items = query ? asyncMap.get(source.key) : source.items;
|
|
67
|
+
|
|
68
|
+
if (items) {
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
results.push({
|
|
71
|
+
globalIndex: results.length,
|
|
72
|
+
sourceKey: source.key,
|
|
73
|
+
sourceLabel: source.label,
|
|
74
|
+
item
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const item of source.items) {
|
|
83
|
+
if (query && !item.label.toLowerCase().includes(query) && !(item.subLabel?.toLowerCase().includes(query) ?? false)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
results.push({
|
|
88
|
+
globalIndex: results.length,
|
|
89
|
+
sourceKey: source.key,
|
|
90
|
+
sourceLabel: source.label,
|
|
91
|
+
item
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const groupedItems = computed<CommandPaletteGroup[]>(() => {
|
|
100
|
+
const items = unref(filteredItems);
|
|
101
|
+
const groups = new Map<string, CommandPaletteGroup>();
|
|
102
|
+
|
|
103
|
+
for (const result of items) {
|
|
104
|
+
if (!groups.has(result.sourceKey)) {
|
|
105
|
+
groups.set(result.sourceKey, {
|
|
106
|
+
sourceKey: result.sourceKey,
|
|
107
|
+
sourceLabel: result.sourceLabel,
|
|
108
|
+
startIndex: result.globalIndex,
|
|
109
|
+
items: []
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
(groups.get(result.sourceKey)!.items as CommandPaletteResultItem[]).push(result);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return Array.from(groups.values());
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const tabs = computed(() => unref(params.sources).filter(source => source.tab));
|
|
120
|
+
|
|
121
|
+
const activeTabSource = computed<FluxCommandSource | null>(() => {
|
|
122
|
+
const tab = unref(activeTab);
|
|
123
|
+
|
|
124
|
+
if (!tab) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return unref(tabs).find(source => source.key === tab) ?? null;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const subActions = computed(() => {
|
|
132
|
+
const actions = unref(subActionTarget)?.subActions ?? [];
|
|
133
|
+
const query = unref(search).toLowerCase().trim();
|
|
134
|
+
|
|
135
|
+
if (!query) {
|
|
136
|
+
return actions;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return actions.filter(action => action.label.toLowerCase().includes(query));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const totalItems = computed(() => {
|
|
143
|
+
if (unref(subActionTarget)) {
|
|
144
|
+
return unref(subActions).length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return unref(filteredItems).length;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
function setSearch(value: string): void {
|
|
151
|
+
search.value = value;
|
|
152
|
+
highlightedIndex.value = value.trim() ? 0 : -1;
|
|
153
|
+
|
|
154
|
+
if (value.trim() && !unref(subActionTarget) && unref(params.sources).some(s => s.fetchSearch)) {
|
|
155
|
+
isLoading.value = true;
|
|
156
|
+
} else {
|
|
157
|
+
isLoading.value = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function setActiveTab(key: string | null): void {
|
|
162
|
+
const allTabs = unref(tabs);
|
|
163
|
+
const tabKeys: (string | null)[] = [null, ...allTabs.map(t => t.key)];
|
|
164
|
+
|
|
165
|
+
isTransitioningBack.value = tabKeys.indexOf(key) < tabKeys.indexOf(unref(activeTab));
|
|
166
|
+
activeTab.value = key;
|
|
167
|
+
search.value = '';
|
|
168
|
+
highlightedIndex.value = -1;
|
|
169
|
+
subActionTarget.value = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectHighlighted(onClose: () => void, onActivate: (item: FluxCommandSourceItem) => void): void {
|
|
173
|
+
const index = unref(highlightedIndex);
|
|
174
|
+
const target = unref(subActionTarget);
|
|
175
|
+
|
|
176
|
+
if (target) {
|
|
177
|
+
const action = unref(subActions)[index];
|
|
178
|
+
|
|
179
|
+
if (action) {
|
|
180
|
+
action.onActivate();
|
|
181
|
+
onClose();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = unref(filteredItems)[index];
|
|
188
|
+
|
|
189
|
+
if (!result) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (result.item.subActions?.length) {
|
|
194
|
+
enterSubActions(result.item);
|
|
195
|
+
} else {
|
|
196
|
+
result.item.onActivate();
|
|
197
|
+
onActivate(result.item);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function enterSubActions(item: FluxCommandSourceItem): void {
|
|
202
|
+
savedState.value = {search: unref(search), highlightedIndex: unref(highlightedIndex)};
|
|
203
|
+
subActionTarget.value = item;
|
|
204
|
+
search.value = '';
|
|
205
|
+
highlightedIndex.value = 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function navigateTab(direction: number): void {
|
|
209
|
+
const allTabs = unref(tabs);
|
|
210
|
+
|
|
211
|
+
if (allTabs.length === 0) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const tabKeys: (string | null)[] = [null, ...allTabs.map(tab => tab.key)];
|
|
216
|
+
const currentIndex = tabKeys.indexOf(unref(activeTab));
|
|
217
|
+
const nextIndex = (currentIndex + direction + tabKeys.length) % tabKeys.length;
|
|
218
|
+
|
|
219
|
+
setActiveTab(tabKeys[nextIndex]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function restoreState(): void {
|
|
223
|
+
subActionTarget.value = null;
|
|
224
|
+
const state = unref(savedState);
|
|
225
|
+
|
|
226
|
+
if (state) {
|
|
227
|
+
search.value = state.search;
|
|
228
|
+
isKeyboardNav.value = true;
|
|
229
|
+
highlightedIndex.value = state.highlightedIndex;
|
|
230
|
+
savedState.value = null;
|
|
231
|
+
} else {
|
|
232
|
+
highlightedIndex.value = -1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function onKeyNavigate(evt: KeyboardEvent, onClose: () => void, onActivate: (item: FluxCommandSourceItem) => void): void {
|
|
237
|
+
const total = unref(totalItems);
|
|
238
|
+
const current = unref(highlightedIndex);
|
|
239
|
+
|
|
240
|
+
switch (evt.key) {
|
|
241
|
+
case 'ArrowDown':
|
|
242
|
+
evt.preventDefault();
|
|
243
|
+
isKeyboardNav.value = true;
|
|
244
|
+
highlightedIndex.value = Math.min(total - 1, current + 1);
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case 'ArrowUp':
|
|
248
|
+
evt.preventDefault();
|
|
249
|
+
isKeyboardNav.value = true;
|
|
250
|
+
highlightedIndex.value = total === 0 ? -1 : Math.max(0, current - 1);
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'ArrowLeft':
|
|
254
|
+
if (!unref(search) && !unref(subActionTarget)) {
|
|
255
|
+
evt.preventDefault();
|
|
256
|
+
navigateTab(-1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case 'ArrowRight':
|
|
262
|
+
if (!unref(search) && !unref(subActionTarget)) {
|
|
263
|
+
evt.preventDefault();
|
|
264
|
+
navigateTab(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'Enter':
|
|
270
|
+
evt.preventDefault();
|
|
271
|
+
selectHighlighted(onClose, onActivate);
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case 'Escape':
|
|
275
|
+
evt.preventDefault();
|
|
276
|
+
|
|
277
|
+
if (unref(subActionTarget)) {
|
|
278
|
+
restoreState();
|
|
279
|
+
} else {
|
|
280
|
+
onClose();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'Backspace':
|
|
286
|
+
if (!unref(search)) {
|
|
287
|
+
if (unref(subActionTarget)) {
|
|
288
|
+
evt.preventDefault();
|
|
289
|
+
restoreState();
|
|
290
|
+
} else if (unref(activeTab)) {
|
|
291
|
+
evt.preventDefault();
|
|
292
|
+
activeTab.value = null;
|
|
293
|
+
highlightedIndex.value = -1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function reset(): void {
|
|
302
|
+
search.value = '';
|
|
303
|
+
activeTab.value = null;
|
|
304
|
+
highlightedIndex.value = -1;
|
|
305
|
+
subActionTarget.value = null;
|
|
306
|
+
asyncResults.value = new Map();
|
|
307
|
+
isLoading.value = false;
|
|
308
|
+
fetchGeneration++;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
watch(debouncedSearch, async (query) => {
|
|
312
|
+
if (unref(subActionTarget)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const trimmed = query.trim();
|
|
317
|
+
|
|
318
|
+
if (!trimmed) {
|
|
319
|
+
asyncResults.value = new Map();
|
|
320
|
+
isLoading.value = false;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const tab = unref(activeTab);
|
|
325
|
+
const sources = unref(params.sources);
|
|
326
|
+
const asyncSources = sources.filter(s => {
|
|
327
|
+
if (!s.fetchSearch) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return !tab || s.key === tab;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (asyncSources.length === 0) {
|
|
335
|
+
isLoading.value = false;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const generation = ++fetchGeneration;
|
|
340
|
+
isLoading.value = true;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const fetched = await Promise.all(
|
|
344
|
+
asyncSources.map(async (source) => ({
|
|
345
|
+
key: source.key,
|
|
346
|
+
items: await source.fetchSearch!(trimmed)
|
|
347
|
+
}))
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
if (generation !== fetchGeneration) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const map = new Map<string, FluxCommandSourceItem[]>();
|
|
355
|
+
|
|
356
|
+
for (const {key, items} of fetched) {
|
|
357
|
+
map.set(key, items);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
asyncResults.value = map;
|
|
361
|
+
} finally {
|
|
362
|
+
if (generation === fetchGeneration) {
|
|
363
|
+
isLoading.value = false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
watch(highlightedIndex, (index) => {
|
|
369
|
+
if (index < 0 || !unref(isKeyboardNav)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
isKeyboardNav.value = false;
|
|
374
|
+
|
|
375
|
+
nextTick(() => unref(params.itemRefs)?.[index]?.$el?.scrollIntoView({block: 'nearest'}));
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
watch(totalItems, (total) => {
|
|
379
|
+
const current = unref(highlightedIndex);
|
|
380
|
+
|
|
381
|
+
if (current >= total) {
|
|
382
|
+
highlightedIndex.value = Math.max(-1, total - 1);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
search,
|
|
388
|
+
activeTab,
|
|
389
|
+
activeTabSource,
|
|
390
|
+
highlightedIndex,
|
|
391
|
+
isLoading,
|
|
392
|
+
isTransitioningBack,
|
|
393
|
+
subActionTarget,
|
|
394
|
+
filteredItems,
|
|
395
|
+
groupedItems,
|
|
396
|
+
subActions,
|
|
397
|
+
tabs,
|
|
398
|
+
totalItems,
|
|
399
|
+
setSearch,
|
|
400
|
+
setActiveTab,
|
|
401
|
+
enterSubActions,
|
|
402
|
+
onKeyNavigate,
|
|
403
|
+
reset
|
|
404
|
+
};
|
|
405
|
+
}
|
|
@@ -14,8 +14,8 @@ export default function (modelValue: Ref<FluxFormSelectValue>, isMultiple: boole
|
|
|
14
14
|
const search = unref(searchQuery)?.trim().toLowerCase();
|
|
15
15
|
|
|
16
16
|
const available = unref(options)
|
|
17
|
-
.filter(o =>
|
|
18
|
-
.filter(o =>
|
|
17
|
+
.filter(o => isFluxFormSelectGroup(o) || (!search || o.label.toLowerCase().includes(search)))
|
|
18
|
+
.filter(o => isFluxFormSelectGroup(o) || !isMultiple || !unref(selected).find(s => s.value === o.value));
|
|
19
19
|
|
|
20
20
|
if (available.length === 0) {
|
|
21
21
|
return [];
|