<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>