@flux-ui/components 3.0.0-next.34 → 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/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/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 +633 -64
- package/dist/index.js.map +1 -1
- package/package.json +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/FluxTableActions.vue +3 -3
- package/src/component/FluxTag.vue +3 -1
- package/src/component/index.ts +4 -1
- package/src/composable/private/useCommandPalette.ts +405 -0
- package/src/css/component/Badge.module.scss +7 -0
- package/src/css/component/CommandPalette.module.scss +332 -0
- /package/dist/component/{FluxActions.vue.d.ts → FluxActionStack.vue.d.ts} +0 -0
- /package/src/component/{FluxActions.vue → FluxActionStack.vue} +0 -0
|
@@ -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
|
+
}
|