2023-06-19 00:21:36 +02:00
|
|
|
<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;
|
2023-10-31 20:27:21 +01:00
|
|
|
searchPrompt.value = '';
|
2023-06-19 00:21:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-06-19 21:51:25 +02:00
|
|
|
const { closeOnSelect } = option;
|
|
|
|
|
2023-06-19 00:21:36 +02:00
|
|
|
if (option.action) {
|
|
|
|
option.action();
|
2023-06-19 21:51:25 +02:00
|
|
|
|
|
|
|
if (closeOnSelect) {
|
|
|
|
close();
|
|
|
|
}
|
|
|
|
|
2023-06-19 00:21:36 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-06-19 21:51:25 +02:00
|
|
|
const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect);
|
|
|
|
|
2023-06-19 00:21:36 +02:00
|
|
|
if (option.to) {
|
|
|
|
router.push(option.to);
|
2023-06-19 21:51:25 +02:00
|
|
|
|
|
|
|
if (closeAfterNavigation) {
|
|
|
|
close();
|
|
|
|
}
|
|
|
|
return;
|
2023-06-19 00:21:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (option.href) {
|
|
|
|
window.open(option.href, '_blank');
|
2023-06-19 21:51:25 +02:00
|
|
|
|
|
|
|
if (closeAfterNavigation) {
|
|
|
|
close();
|
|
|
|
}
|
2023-06-19 00:21:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
</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 />
|
2023-11-01 15:38:19 +01:00
|
|
|
{{ $t('search.label') }}
|
2023-06-19 00:21:36 +02:00
|
|
|
|
|
|
|
<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">
|
2025-01-12 22:16:52 +01:00
|
|
|
<div ml-3 mt-3 text-sm text-primary font-bold op-60>
|
2023-06-19 00:21:36 +02:00
|
|
|
{{ 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>
|