Files
hoppscotch/packages/hoppscotch-common/src/components/app/spotlight/index.vue

215 lines
5.9 KiB
Vue

<template>
<HoppSmartModal
v-if="show"
styles="sm:max-w-lg"
full-width
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col border-b transition border-dividerLight">
<div class="flex items-center p-6 space-x-2">
<input
id="command"
v-model="search"
v-focus
type="text"
autocomplete="off"
name="command"
:placeholder="`${t('app.type_a_command_search')}`"
class="flex flex-1 text-base bg-transparent text-secondaryDark"
/>
<icon-lucide-refresh-cw
v-if="searchSession?.loading"
class="animate-spin"
/>
</div>
<div
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
>
<div class="flex items-center">
<kbd class="shortcut-key"></kbd>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_navigate") }}
</span>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_select") }}
</span>
</div>
<div class="flex items-center">
<kbd class="shortcut-key">ESC</kbd>
<span class="ml-2 truncate">
{{ t("action.to_close") }}
</span>
</div>
</div>
</div>
<div
v-if="searchSession"
class="flex flex-col flex-1 overflow-auto space-y-4 divide-y divide-dividerLight"
>
<div
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
:key="`section-${sectionID}`"
class="flex flex-col"
>
<h5 class="px-6 py-2 my-2 text-secondaryLight">
{{ sectionResult.title }}
</h5>
<AppSpotlightEntry
v-for="(result, entryIndex) in sectionResult.results"
:key="`result-${result.id}`"
:entry="result"
:active="isEqual(selectedEntry, [sectionIndex, entryIndex])"
@mouseover="selectedEntry = [sectionIndex, entryIndex]"
@action="runAction(sectionID, result)"
/>
</div>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue"
import { useService } from "dioc/vue"
import { useI18n } from "@composables/i18n"
import {
SpotlightService,
SpotlightSearchState,
SpotlightSearcherResult,
} from "~/services/spotlight"
import { isEqual } from "lodash-es"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
const t = useI18n()
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const spotlightService = useService(SpotlightService)
useService(HistorySpotlightSearcherService)
useService(UserSpotlightSearcherService)
const search = ref("")
const searchSession = ref<SpotlightSearchState>()
const stopSearchSession = ref<() => void>()
const scoredResults = computed(() =>
Object.entries(searchSession.value?.results ?? {}).sort(
([, sectionA], [, sectionB]) => sectionB.avgScore - sectionA.avgScore
)
)
const { selectedEntry } = newUseArrowKeysForNavigation()
watch(
() => props.show,
(show) => {
search.value = ""
if (show) {
const [session, onSessionEnd] =
spotlightService.createSearchSession(search)
searchSession.value = session.value
stopSearchSession.value = onSessionEnd
} else {
stopSearchSession.value?.()
stopSearchSession.value = undefined
searchSession.value = undefined
}
}
)
function runAction(searcherID: string, result: SpotlightSearcherResult) {
spotlightService.selectSearchResult(searcherID, result)
emit("hide-modal")
}
function newUseArrowKeysForNavigation() {
const selectedEntry = ref<[number, number]>([0, 0]) // [sectionIndex, entryIndex]
watch(search, () => {
selectedEntry.value = [0, 0]
})
const onArrowDown = () => {
const [sectionIndex, entryIndex] = selectedEntry.value
const [, section] = scoredResults.value[sectionIndex]
if (entryIndex < section.results.length - 1) {
selectedEntry.value = [sectionIndex, entryIndex + 1]
} else if (sectionIndex < scoredResults.value.length - 1) {
selectedEntry.value = [sectionIndex + 1, 0]
} else {
selectedEntry.value = [0, 0]
}
}
const onArrowUp = () => {
const [sectionIndex, entryIndex] = selectedEntry.value
if (entryIndex > 0) {
selectedEntry.value = [sectionIndex, entryIndex - 1]
} else if (sectionIndex > 0) {
const [, section] = scoredResults.value[sectionIndex - 1]
selectedEntry.value = [sectionIndex - 1, section.results.length - 1]
} else {
selectedEntry.value = [
scoredResults.value.length - 1,
scoredResults.value[scoredResults.value.length - 1][1].results.length -
1,
]
}
}
function handleKeyPress(e: KeyboardEvent) {
if (e.key === "ArrowUp") {
e.preventDefault()
e.stopPropagation()
onArrowUp()
} else if (e.key === "ArrowDown") {
e.preventDefault()
e.stopPropagation()
onArrowDown()
} else if (e.key === "Enter") {
e.preventDefault()
e.stopPropagation()
const [sectionIndex, entryIndex] = selectedEntry.value
const [sectionID, section] = scoredResults.value[sectionIndex]
const result = section.results[entryIndex]
runAction(sectionID, result)
}
}
watch(
() => props.show,
(show) => {
if (show) {
window.addEventListener("keydown", handleKeyPress)
} else {
window.removeEventListener("keydown", handleKeyPress)
}
}
)
return { selectedEntry }
}
</script>