Search.vue 6.22 KiB
<template>
<div
class="search-container h-100 d-flex align-items-center"
:class="{ 'full-width': fullWidth, closed: !isExpanded }"
>
<v-text-field
placeholder="Search"
data-cy="search"
filled
dense
clearable
full-width
:hide-details="true"
class="expanding-search mt-n1"
v-model="searchText"
prepend-inner-icon="mdi-magnify"
@focus="expand"
@blur="blur"
@input="change"
:loading="searchLoading"
>
<template v-slot:progress>
<v-progress-linear
indeterminate
absolute
color="primary"
></v-progress-linear>
</template>
</v-text-field>
<div class="results-container">
<div
data-cy="emptySearchResults"
class="empty-results"
v-if="
searchText &&
!searchLoading &&
displayResults &&
displayResults.length === 0
"
>
No search results found
</div>
<div
data-cy="searchResults"
class="results"
v-else-if="displayResults && displayResults.length > 0"
>
<template v-for="(result, index) in displayResults">
<div class="result" :key="index">
<a class="results-link" :href="result.url">
<cite>{{ result.url }}</cite>
<span class="title">{{ result.title }}</span>
</a>
<div
class="results-description text-left"
v-if="result.match.description"
v-html="result.match.description"
></div>
<div
class="results-content text-left"
v-if="result.match.content"
v-html="result.match.content"
></div>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
import Search from "@/api/search.js";
import { routesByPath } from "@/router/routes.js";
export default {
name: "SearchComponent",
props: {
fullWidth: {
type: Boolean,
default: false,
},
},
data: () => ({
searchClosed: true,
searchText: null,
searchLoading: false,
debounce: null,
searchApi: null,
displayResults: null,
}),
mounted() {
this.searchApi = new Search();
},
methods: {
expand() {
this.searchClosed = false;
this.$emit("expanded");
},
blur() {
this.searchClosed = true;
this.searchLoading = false;
if (!this.isExpanded) {
this.$emit("collapsed");
}
},
change() {
// cancel any pending search (debounce)
if (this.debounce) {
clearTimeout(this.debounce);
}
// empty text short-circuit
if (!this.searchText) {
this.displayResults = [];
return;
}
this.searchLoading = true;
// debounce the call to search to only execute once per time window
this.debounce = setTimeout(async () => {
this.search();
this.searchLoading = false;
}, 300);
},
/**
* Perform a search using the search api
*/
search() {
const searchResults = this.searchApi.search(this.searchText);
this.displayResults = searchResults.map((result) => ({
url: `${window.location.origin}${result.ref}`,
title:
routesByPath[result.ref]?.meta.title ||
routesByPath[result.ref]?.header?.title,
match: this.getMatch(result),
}));
},
/**
* Logic to calculate the match details of a specific result to display.
*
* @returns {Object} matchResult
* @returns {String} matchResult.content - html string for content, including highlights
* @returns {String} matchResult.description - html string for description, including highlights
*/
getMatch(result) {
const context = this.searchApi.getContext(result, 140);
const bestContentContext = this.searchApi.getBestContentContext(context);
let content = bestContentContext;
let description = "";
for (const matchMeta of Object.values(context)) {
if (matchMeta.content) {
content = this.searchApi.highlight(matchMeta.content, content);
}
if (matchMeta.description) {
description = this.searchApi.highlight(
matchMeta.description,
description
);
}
}
return {
content,
description,
};
},
},
computed: {
isExpanded() {
return !this.searchClosed || this.searchText;
},
},
};
</script>
<style lang="scss" scoped>
.search-container {
&.full-width {
.expanding-search,
.results-container {
max-width: 100%;
}
}
.expanding-search,
.results-container {
position: absolute;
right: 0;
width: 100%;
max-width: 800px;
}
&.full-width.closed {
.expanding-search {
right: 64px;
}
}
&.closed {
.expanding-search {
max-width: 45px;
:deep(.v-input__slot) {
cursor: pointer !important;
background: transparent !important;
:deep(.v-input__icon--append) {
display: none;
}
}
}
}
.expanding-search {
transition: max-width 0.25s;
min-width: 45px;
:deep(.v-input__slot) {
&:before,
&:after {
border-color: transparent !important;
background-color: transparent;
}
}
}
.results-container {
top: 68px;
background-color: #021827;
max-height: 80vh;
overflow-y: scroll;
padding-left: 16px;
padding-right: 16px;
.results > .result {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 32px;
margin-bottom: 32px;
&:first-child {
margin-top: 16px;
}
&:last-child {
margin-bottom: 16px;
}
color: #fcfafa;
.results-link {
text-align: left;
&:hover {
color: lighten($link-color, 10%) !important;
}
cite {
font-style: normal;
color: #fcfafa;
display: block;
font-size: 14px;
}
}
.results-content {
color: #bdc1c6;
}
:deep(em) {
font-weight: bold;
font-style: normal;
color: $p1-light-green;
}
}
}
}
</style>