Skip to content

Commit b7315ff

Browse files
Implement page search URL + Opensearch XML (#2143)
* Implement a page search URL There are two pieces to this: - Prefill and execute search when a page loads with the `search` param set - Update the URL bar when a search is executed, with 500ms debounce * Add opensearch XML for automatic search engine Also fixup some TS lint errors in the new component, language server wasn't working before. * Clear search query on modal dismissal/navigation Use a visibility observer for the input box, which clears the query when it's dismssed. Also use manual `input` dispatch instead of `initialQuery`, so that the initial query is dismissed after the first search runs. * Remove unused docsearch options * Harden URLDocSearch init --------- Co-authored-by: hustcer <hustcer@outlook.com>
1 parent 6e1900f commit b7315ff

5 files changed

Lines changed: 132 additions & 0 deletions

File tree

.vuepress/client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ExperimentalOption from './components/ExperimentalOption.vue';
1111
import JumpToc from './components/JumpToc.vue';
1212
import PrBy from './components/PrBy.vue';
1313
import ReleaseToc from './components/ReleaseToc.vue';
14+
import URLDocSearch from './components/URLDocSearch.vue';
1415

1516
export default defineClientConfig({
1617
enhance({ app }) {
@@ -20,5 +21,8 @@ export default defineClientConfig({
2021
app.component('JumpToc', JumpToc);
2122
app.component('PrBy', PrBy);
2223
app.component('ReleaseToc', ReleaseToc);
24+
25+
// Override the builtin searchbox
26+
app.component('SearchBox', URLDocSearch);
2327
},
2428
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<template>
2+
<DocSearch />
3+
</template>
4+
5+
<script setup lang="ts">
6+
import { DocSearch } from '@vuepress/plugin-docsearch/client';
7+
import {
8+
useDebounceFn,
9+
useElementVisibility,
10+
useEventListener,
11+
} from '@vueuse/core';
12+
import { onMounted, ref, watch } from 'vue';
13+
import { useRouter, useRoute } from 'vue-router';
14+
15+
const SEARCH_KEY = 'search';
16+
const SEARCH_INPUT_ID = 'docsearch-input';
17+
18+
const router = useRouter();
19+
const route = useRoute();
20+
21+
const inputElement = ref<HTMLInputElement>();
22+
const isNavigating = ref(false);
23+
24+
// Handle initial search query, if one is set
25+
onMounted(() => {
26+
const query = new URL(window.location.href).searchParams.get(SEARCH_KEY);
27+
if (query) {
28+
const button =
29+
document.querySelector<HTMLButtonElement>('.DocSearch-Button');
30+
if (!button) return;
31+
button.click();
32+
// Set value in the input element once it appears
33+
performInitialQuery(query);
34+
}
35+
});
36+
37+
function performInitialQuery(query: string) {
38+
const found = document.getElementById(SEARCH_INPUT_ID);
39+
if (found) {
40+
inputElement.value = found as HTMLInputElement;
41+
inputElement.value.value = query;
42+
inputElement.value.dispatchEvent(new Event('input'));
43+
} else {
44+
setTimeout(() => performInitialQuery(query), 50);
45+
}
46+
}
47+
48+
// There's some debounce builtin to docsearch, this mimics that and should
49+
// help prevent browser history from getting filled with partial queries.
50+
const setURLQueryDebounced = useDebounceFn(setURLQuery, 500);
51+
52+
// When the user types a search query, update URL query param accordingly
53+
useEventListener('input', (event) => {
54+
const target = event.target as HTMLInputElement | undefined;
55+
const searchQuery = target?.value;
56+
if (target?.id !== SEARCH_INPUT_ID) {
57+
return;
58+
}
59+
inputElement.value = target;
60+
setURLQueryDebounced(searchQuery);
61+
});
62+
63+
// Clear the URL query param when search input is reset (i.e. "Clear" button).
64+
useEventListener('reset', (event) => {
65+
const target = event.target as HTMLFormElement | undefined;
66+
if (target?.classList.contains('DocSearch-Form')) {
67+
setURLQuery();
68+
}
69+
});
70+
71+
// Clear the URL query param when the search modal is dismissed.
72+
// NOTE: newer versions of @docsearch/js also provide a callback option for this.
73+
const inputIsVisible = useElementVisibility(inputElement);
74+
watch(inputIsVisible, (isVisible, wasVisible) => {
75+
if (wasVisible && !isVisible && !isNavigating.value) {
76+
setURLQuery();
77+
}
78+
});
79+
80+
// When a search result is selected, the modal is dismissed and its route is pushed, without `?search=`.
81+
// Track this to avoid running visibility watch logic to clear the URL in this case.
82+
router.beforeEach((route) => {
83+
if (!route.query[SEARCH_KEY]) {
84+
isNavigating.value = true;
85+
}
86+
});
87+
router.afterEach(() => {
88+
isNavigating.value = false;
89+
});
90+
91+
// Set `?search=` query param; if passed empty string or undefined, clear the param instead.
92+
function setURLQuery(newSearch?: string) {
93+
const { path, query: oldQuery, hash } = route;
94+
const { [SEARCH_KEY]: oldSearch, ...query } = oldQuery;
95+
96+
// Replace the history entry if the only the search query changed, to avoid
97+
// polluting the user's browser history with partial/incomplete search queries.
98+
const replace = oldSearch !== undefined || newSearch === undefined;
99+
100+
if (newSearch) {
101+
query[SEARCH_KEY] = newSearch;
102+
}
103+
104+
router.push({ path, query, hash, replace });
105+
}
106+
</script>

.vuepress/config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ export default defineUserConfig({
110110
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black' },
111111
],
112112
['link', { rel: 'icon', href: '/icon.png' }],
113+
[
114+
'link',
115+
{
116+
rel: 'search',
117+
type: 'application/opensearchdescription+xml',
118+
title: 'Nushell Docs', // NOTE: must match ShortName
119+
href: '/opensearch.xml',
120+
},
121+
],
113122
],
114123
markdown: {
115124
importCode: {

.vuepress/public/opensearch.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<OpenSearchDescription
2+
xmlns="http://a9.com/-/spec/opensearch/1.1/"
3+
xmlns:moz="http://www.mozilla.org/2006/browser/search/"
4+
>
5+
<ShortName>Nushell Docs</ShortName>
6+
<Description>Search Nushell documentation</Description>
7+
<InputEncoding>UTF-8</InputEncoding>
8+
<Image width="16" height="16" type="image/png">https://www.nushell.sh/icon.png</Image>
9+
<Url type="text/html" template="https://www.nushell.sh/?search={searchTerms}" />
10+
<Url type="application/opensearchdescription+xml" rel="self" template="https://www.nushell.sh/opensearch.xml" />
11+
<!-- TODO: it might be possible to provide suggestions via some Algolia API? -->
12+
</OpenSearchDescription>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@vuepress/plugin-shiki": "2.0.0-rc.118",
2121
"@vuepress/plugin-sitemap": "2.0.0-rc.118",
2222
"@vuepress/theme-default": "2.0.0-rc.118",
23+
"@vueuse/core": "^14.0.0",
2324
"asciinema-player": "^3.15.1",
2425
"cross-env": "^10.1.0",
2526
"lefthook": "1.8.2",

0 commit comments

Comments
 (0)