1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import _ from 'lodash';
import { useCommandPaletteStore } from './command-palette.store';
import type { PaletteOption } from './command-palette.types';
const isModalOpen = ref(false);
const inputRef = ref();
const router = useRouter();
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
const commandPaletteStore = useCommandPaletteStore();
const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore);
const keys = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
},
});
whenever(isModalOpen, () => inputRef.value?.focus());
whenever(keys.ctrl_k, open);
whenever(keys.meta_k, open);
whenever(keys.escape, close);
function open() {
return isModalOpen.value = true;
}
function close() {
isModalOpen.value = false;
searchPrompt.value = '';
}
const selectedOptionIndex = ref(0);
function handleKeydown(event: KeyboardEvent) {
const { key } = event;
const isEnterPressed = key === 'Enter';
const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key);
const isArrowDown = key === 'ArrowDown';
if (isArrowUpOrDown) {
const increment = isArrowDown ? 1 : -1;
const maxIndex = Math.max(_.chain(filteredSearchResult.value).values().flatten().size().value() - 1, 0);
selectedOptionIndex.value = Math.min(Math.max(selectedOptionIndex.value + increment, 0), maxIndex);
return;
}
if (isEnterPressed) {
const option = _.chain(filteredSearchResult.value)
.values()
.flatten()
.nth(selectedOptionIndex.value)
.value();
activateOption(option);
}
}
function getOptionIndex(option: PaletteOption) {
return _.chain(filteredSearchResult.value)
.values()
.flatten()
.findIndex(o => o === option)
.value();
}
function activateOption(option: PaletteOption) {
const { closeOnSelect } = option;
if (option.action) {
option.action();
if (closeOnSelect) {
close();
}
return;
}
const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect);
if (option.to) {
router.push(option.to);
if (closeAfterNavigation) {
close();
}
return;
}
if (option.href) {
window.open(option.href, '_blank');
if (closeAfterNavigation) {
close();
}
}
}
</script>
<template>
<div flex-1>
<c-button w-full important:justify-start @click="isModalOpen = true">
<span flex items-center gap-3 op-40>
<icon-mdi-search />
{{ $t('search.label') }}
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
{{ isMac ? 'Cmd' : 'Ctrl' }} + K
</span>
</span>
</c-button>
<c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown">
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
<div v-for="(options, category) in filteredSearchResult" :key="category">
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
{{ category }}
</div>
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
</div>
</c-modal>
</div>
</template>
<style scoped lang="less">
.c-input-text {
font-size: 18px;
::v-deep(.input-wrapper) {
padding: 4px;
padding-left: 18px;
}
}
.c-modal--overlay {
align-items: flex-start !important;
padding-top: 80px;
}
</style>
|