Skip to content

Commit f079085

Browse files
committed
fix: adapt instant search into user preferences
1 parent 229227b commit f079085

File tree

5 files changed

+80
-64
lines changed

5 files changed

+80
-64
lines changed

app/app.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ if (import.meta.server) {
4848
}
4949
5050
const keyboardShortcuts = useKeyboardShortcuts()
51-
const { settings } = useSettings()
51+
const instantSearch = useInstantSearch()
5252
5353
onKeyDown(
5454
'/',
5555
e => {
5656
if (e.ctrlKey) {
5757
e.preventDefault()
58-
settings.value.instantSearch = !settings.value.instantSearch
58+
instantSearch.value = !instantSearch.value
5959
return
6060
}
6161

app/components/InstantSearch.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<script setup lang="ts">
2-
import { useSettings } from '~/composables/useSettings'
3-
4-
const { settings } = useSettings()
2+
const instantSearch = useInstantSearch()
53
64
onPrehydrate(el => {
7-
const settingsSaved = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
8-
const enabled = settingsSaved.instantSearch
5+
let userPreferences: Record<string, unknown> = {}
6+
7+
try {
8+
userPreferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
9+
} catch {}
10+
11+
const enabled = userPreferences.instantSearch
912
if (enabled === false) {
1013
el.querySelector('[data-instant-search-on]')!.className = 'hidden'
1114
el.querySelector('[data-instant-search-off]')!.className = ''
@@ -20,7 +23,7 @@ onPrehydrate(el => {
2023
style="font-size: 0.8em"
2124
aria-hidden="true"
2225
/>
23-
<span data-instant-search-on :class="settings.instantSearch ? '' : 'hidden'">
26+
<span data-instant-search-on :class="instantSearch ? '' : 'hidden'">
2427
<i18n-t keypath="search.instant_search_advisory">
2528
<template #label>
2629
{{ $t('search.instant_search') }}
@@ -29,13 +32,13 @@ onPrehydrate(el => {
2932
<strong>{{ $t('search.instant_search_on') }}</strong>
3033
</template>
3134
<template #action>
32-
<button type="button" class="underline" @click="settings.instantSearch = false">
35+
<button type="button" class="underline" @click="instantSearch = false">
3336
{{ $t('search.instant_search_turn_off') }}
3437
</button>
3538
</template>
3639
</i18n-t>
3740
</span>
38-
<span data-instant-search-off :class="settings.instantSearch ? 'hidden' : ''">
41+
<span data-instant-search-off :class="instantSearch ? 'hidden' : ''">
3942
<i18n-t keypath="search.instant_search_advisory">
4043
<template #label>
4144
{{ $t('search.instant_search') }}
@@ -44,7 +47,7 @@ onPrehydrate(el => {
4447
<strong>{{ $t('search.instant_search_off') }}</strong>
4548
</template>
4649
<template #action>
47-
<button type="button" class="underline" @click="settings.instantSearch = true">
50+
<button type="button" class="underline" @click="instantSearch = true">
4851
{{ $t('search.instant_search_turn_on') }}
4952
</button>
5053
</template>

app/composables/useGlobalSearch.ts

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,117 @@
1-
import { normalizeSearchParam } from '#shared/utils/url'
2-
import { debounce } from 'perfect-debounce'
1+
import { normalizeSearchParam } from "#shared/utils/url";
2+
import { debounce } from "perfect-debounce";
33

44
// Pages that have their own local filter using ?q
5-
const pagesWithLocalFilter = new Set(['~username', 'org'])
5+
const pagesWithLocalFilter = new Set(["~username", "org"]);
66

7-
const SEARCH_DEBOUNCE_MS = 100
7+
const SEARCH_DEBOUNCE_MS = 100;
88

9-
export function useGlobalSearch(place: 'header' | 'content' = 'content') {
10-
const { settings } = useSettings()
11-
const { searchProvider } = useSearchProvider()
9+
export function useGlobalSearch(place: "header" | "content" = "content") {
10+
const instantSearch = useInstantSearch();
11+
const { searchProvider } = useSearchProvider();
1212
const searchProviderValue = computed(() => {
13-
const p = normalizeSearchParam(route.query.p)
14-
if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
15-
return 'algolia'
16-
})
13+
const p = normalizeSearchParam(route.query.p);
14+
if (p === "npm" || searchProvider.value === "npm") return "npm";
15+
return "algolia";
16+
});
1717

18-
const router = useRouter()
19-
const route = useRoute()
18+
const router = useRouter();
19+
const route = useRoute();
2020
// Internally used searchQuery state
21-
const searchQuery = useState<string>('search-query', () => {
21+
const searchQuery = useState<string>("search-query", () => {
2222
if (pagesWithLocalFilter.has(route.name as string)) {
23-
return ''
23+
return "";
2424
}
25-
return normalizeSearchParam(route.query.q)
26-
})
25+
return normalizeSearchParam(route.query.q);
26+
});
2727

2828
// Committed search query: last value submitted by user
2929
// Syncs instantly when instantSearch is on, but only on Enter press when off
30-
const committedSearchQuery = useState<string>('committed-search-query', () => searchQuery.value)
30+
const committedSearchQuery = useState<string>("committed-search-query", () => searchQuery.value);
3131

3232
const commitSearchQuery = debounce((val: string) => {
33-
committedSearchQuery.value = val
34-
}, SEARCH_DEBOUNCE_MS)
33+
committedSearchQuery.value = val;
34+
}, SEARCH_DEBOUNCE_MS);
3535

3636
// This is basically doing instant search as user types
37-
watch(searchQuery, val => {
38-
if (settings.value.instantSearch) {
39-
commitSearchQuery(val)
37+
watch(searchQuery, (val) => {
38+
if (instantSearch.value) {
39+
commitSearchQuery(val);
4040
}
41-
})
41+
});
4242

4343
// clean search input when navigating away from search page
4444
watch(
4545
() => route.query.q,
46-
urlQuery => {
47-
const value = normalizeSearchParam(urlQuery)
48-
if (!value) searchQuery.value = ''
49-
if (!searchQuery.value) searchQuery.value = value
46+
(urlQuery) => {
47+
const value = normalizeSearchParam(urlQuery);
48+
if (!value) searchQuery.value = "";
49+
if (!searchQuery.value) searchQuery.value = value;
5050
},
51-
)
51+
);
5252

5353
// Updates URL when search query changes (immediately for instantSearch or after Enter hit otherwise)
54-
const updateUrlQueryImpl = (value: string, provider: 'npm' | 'algolia') => {
55-
const isSameQuery = route.query.q === value && route.query.p === provider
54+
const updateUrlQueryImpl = (value: string, provider: "npm" | "algolia") => {
55+
const isSameQuery = route.query.q === value && route.query.p === provider;
5656
// Don't navigate away from pages that use ?q for local filtering
57-
if ((pagesWithLocalFilter.has(route.name as string) && place === 'content') || isSameQuery) {
58-
return
57+
if ((pagesWithLocalFilter.has(route.name as string) && place === "content") || isSameQuery) {
58+
return;
5959
}
6060

61-
if (route.name === 'search') {
61+
if (route.name === "search") {
6262
router.replace({
6363
query: {
6464
...route.query,
6565
q: value || undefined,
66-
p: provider === 'npm' ? 'npm' : undefined,
66+
p: provider === "npm" ? "npm" : undefined,
6767
},
68-
})
69-
return
68+
});
69+
return;
7070
}
7171
router.push({
72-
name: 'search',
72+
name: "search",
7373
query: {
7474
q: value,
75-
p: provider === 'npm' ? 'npm' : undefined,
75+
p: provider === "npm" ? "npm" : undefined,
7676
},
77-
})
78-
}
77+
});
78+
};
7979

80-
const updateUrlQuery = debounce(updateUrlQueryImpl, SEARCH_DEBOUNCE_MS)
80+
const updateUrlQuery = debounce(updateUrlQueryImpl, SEARCH_DEBOUNCE_MS);
8181

8282
function flushUpdateUrlQuery() {
8383
// Commit the current query when explicitly submitted (Enter pressed)
84-
commitSearchQuery.cancel()
85-
committedSearchQuery.value = searchQuery.value
84+
commitSearchQuery.cancel();
85+
committedSearchQuery.value = searchQuery.value;
8686
// When instant search is off the debounce queue is empty, so call directly
87-
if (!settings.value.instantSearch) {
88-
updateUrlQueryImpl(searchQuery.value, searchProvider.value)
87+
if (!instantSearch.value) {
88+
updateUrlQueryImpl(searchQuery.value, searchProvider.value);
8989
} else {
90-
updateUrlQuery.flush()
90+
updateUrlQuery.flush();
9191
}
9292
}
9393

9494
const searchQueryValue = computed({
9595
get: () => searchQuery.value,
9696
set: async (value: string) => {
97-
searchQuery.value = value
97+
searchQuery.value = value;
9898

9999
// When instant search is off, skip debounced URL updates
100100
// Only explicitly called flushUpdateUrlQuery commits and navigates
101-
if (!settings.value.instantSearch) return
101+
if (!instantSearch.value) return;
102102

103103
// Leading debounce implementation as it doesn't work properly out of the box (https://github.com/unjs/perfect-debounce/issues/43)
104104
if (!updateUrlQuery.isPending()) {
105-
updateUrlQueryImpl(value, searchProvider.value)
105+
updateUrlQueryImpl(value, searchProvider.value);
106106
}
107-
updateUrlQuery(value, searchProvider.value)
107+
updateUrlQuery(value, searchProvider.value);
108108
},
109-
})
109+
});
110110

111111
return {
112112
model: searchQueryValue,
113113
committedModel: committedSearchQuery,
114114
provider: searchProviderValue,
115115
startSearch: flushUpdateUrlQuery,
116-
}
116+
};
117117
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const useInstantSearch = createSharedComposable(function useInstantSearch() {
2+
const { preferences } = useUserPreferencesState()
3+
4+
return computed({
5+
get: () => preferences.value.instantSearch ?? true,
6+
set: (value: boolean) => {
7+
preferences.value.instantSearch = value
8+
},
9+
})
10+
})

shared/schemas/userPreferences.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const UserPreferencesSchema = object({
2828
searchProvider: optional(SearchProviderSchema),
2929
/** Whether keyboard shortcuts are enabled globally */
3030
keyboardShortcuts: optional(boolean()),
31+
/** Whether search runs as user types (vs requiring explicit submit) */
32+
instantSearch: optional(boolean()),
3133
/** Timestamp of last update (ISO 8601) - managed by server */
3234
updatedAt: optional(string()),
3335
})
@@ -53,6 +55,7 @@ export const DEFAULT_USER_PREFERENCES: Required<Omit<UserPreferences, 'updatedAt
5355
colorModePreference: null,
5456
searchProvider: import.meta.test ? 'npm' : 'algolia',
5557
keyboardShortcuts: true,
58+
instantSearch: true,
5659
}
5760

5861
export const USER_PREFERENCES_STORAGE_BASE = 'npmx-kv-user-preferences'

0 commit comments

Comments
 (0)