Skip to content

Commit

Permalink
feat: add search components & enhancements (#467)
Browse files Browse the repository at this point in the history
* feat: add search components & enhancements

* chore: extendKeys & keys add more description

* chore: migrating fuse extendKeys config fields
  • Loading branch information
WRXinYue authored Nov 8, 2024
1 parent 713aa9a commit 55ff977
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 79 deletions.
61 changes: 7 additions & 54 deletions packages/valaxy-theme-yun/components/YunFuseSearch.vue
Original file line number Diff line number Diff line change
@@ -1,70 +1,23 @@
<script lang="ts" setup>
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import { useFuse } from '@vueuse/integrations/useFuse'
import { computed, ref, watch } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSiteConfig } from 'valaxy'
import type { FuseListItem } from 'valaxy/types'
import { useFuseSearch } from 'valaxy'
import { isClient, onClickOutside, useScrollLock } from '@vueuse/core'
const props = defineProps<{
defineProps<{
open: boolean
}>()
const emit = defineEmits(['close'])
const searchContainer = ref<HTMLElement>()
const isLocked = useScrollLock(isClient ? document.body : null)
const { t } = useI18n()
const fuseListData = ref<FuseListItem[]>([])
const siteConfig = useSiteConfig()
const keys = computed(() => {
const ks = siteConfig.value.fuse.options.keys || []
return ks.length === 0
? ['title', 'tags', 'categories', 'excerpt']
: ks
})
const input = ref('')
// todo export options
const useFuseOptions = computed<UseFuseOptions<FuseListItem>>(() => ({
fuseOptions: {
includeMatches: true,
findAllMatches: true,
...siteConfig.value.fuse.options,
keys: keys.value,
// threshold: 0.99,
// ignoreLocation: true,
},
// resultLimit: resultLimit.value,
// matchAllWhenSearchEmpty: matchAllWhenSearchEmpty.value,
}))
const { results } = useFuse(input, fuseListData, useFuseOptions)
const isLocked = useScrollLock(isClient ? document.documentElement : null)
const { t } = useI18n()
const { results } = useFuseSearch(input)
const searchInputRef = ref<HTMLInputElement>()
watch(() => props.open, async () => {
if (!props.open)
return
const fuseListDataPath = siteConfig.value.fuse.dataPath.startsWith('http')
? siteConfig.value.fuse.dataPath
: `${import.meta.env.BASE_URL}${siteConfig.value.fuse.dataPath}`
fetch(fuseListDataPath)
.then(res => res.json())
.then((data) => {
if (Array.isArray(data))
fuseListData.value = data
searchInputRef.value?.focus()
})
})
const searchContainer = ref<HTMLElement>()
onClickOutside(searchInputRef, () => {
// emit('close')
Expand Down
3 changes: 2 additions & 1 deletion packages/valaxy-theme-yun/components/YunSearchBtn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ function onClick() {
<template>
<button
class="yun-search-btn popup-trigger size-12 inline-flex justify-center items-center"
:class="!open && 'hover-bg-white/80 hover:bg-black/80'"
text="xl $va-c-text"
hover="bg-white/80 dark:bg-black/80"
:title="t('menu.search')"
@click="onClick"
>
Expand All @@ -35,5 +35,6 @@ function onClick() {
<style lang="scss">
.yun-search-btn {
z-index: var(--yun-z-search-btn);
transition: background-color var(--va-transition-duration);
}
</style>
13 changes: 2 additions & 11 deletions packages/valaxy-theme-yun/components/YunSearchTrigger.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useSiteConfig } from 'valaxy'
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
import { useEventListener } from '@vueuse/core'
import { useHotKey } from '../composables'
const siteConfig = useSiteConfig()
Expand All @@ -14,20 +14,11 @@ function togglePopup() {
open.value = !open.value
}
function handleSearchHotKey(event: KeyboardEvent) {
if (
(event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey))
) {
event.preventDefault()
togglePopup()
}
}
const algoliaRef = ref()
onMounted(() => {
// algolia has its own hotkey
if (isFuse.value)
useEventListener('keydown', handleSearchHotKey)
useHotKey('k', togglePopup)
})
function openSearch() {
Expand Down
17 changes: 16 additions & 1 deletion packages/valaxy-theme-yun/composables/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ref, watch } from 'vue'
import { isClient } from '@vueuse/core'
import { isClient, useEventListener } from '@vueuse/core'

/**
* fetch data from source, and random
Expand All @@ -25,3 +25,18 @@ export function useRandomData<T>(source: string | T[], random = false) {
data,
}
}

export function useHotKey(key: string, callback: () => void) {
const isHotKeyActive = ref(false)

function handleHotKey(event: KeyboardEvent) {
if (event.key.toLowerCase() === key.toLowerCase() && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
callback()
}
}

useEventListener('keydown', handleHotKey)

return { isHotKeyActive }
}
1 change: 1 addition & 0 deletions packages/valaxy/client/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './outline'
// utils
export * from './back'
export * from './decrypt'
export * from './search'

// app
export * from './app'
57 changes: 57 additions & 0 deletions packages/valaxy/client/composables/search/fuse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { computed, onMounted, shallowRef } from 'vue'
import { useSiteConfig } from 'valaxy'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import { useFuse } from '@vueuse/integrations/useFuse'
import type { FuseListItem } from 'valaxy/types'

export function useFuseSearch<T extends FuseListItem = FuseListItem>(
search: MaybeRefOrGetter<string>,
options?: MaybeRefOrGetter<UseFuseOptions<T>>,
) {
const siteConfig = useSiteConfig()

const fuseListData = shallowRef<T[]>([])

const keys = computed(() => {
const ks = siteConfig.value.fuse.options.keys || []
return ks.length === 0 ? ['title', 'tags', 'categories', 'excerpt'] : ks
})

const defaultOptions: UseFuseOptions<T> = {
fuseOptions: {
includeMatches: true,
findAllMatches: true,
// threshold: 0.99,
// ignoreLocation: true,

...siteConfig.value.fuse.options,
keys: keys.value,
},
// resultLimit: resultLimit.value,
// matchAllWhenSearchEmpty: matchAllWhenSearchEmpty.value,
}
const useFuseOptions = computed<UseFuseOptions<T>>(() => ({
...defaultOptions,
...options,
}))

const ruse = useFuse<T>(search, fuseListData, useFuseOptions)

async function fetchFuseListData(path?: string) {
const fuseListDataPath = path
|| (siteConfig.value.fuse.dataPath.startsWith('http')
? siteConfig.value.fuse.dataPath
: `${import.meta.env.BASE_URL}${siteConfig.value.fuse.dataPath}`)

const res = await fetch(fuseListDataPath)
const data = await res.json()

if (Array.isArray(data))
fuseListData.value = data
}

onMounted(fetchFuseListData)

return ruse
}
1 change: 1 addition & 0 deletions packages/valaxy/client/composables/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './fuse'
23 changes: 13 additions & 10 deletions packages/valaxy/node/modules/fuse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import matter from 'gray-matter'
import { cyan, dim } from 'picocolors'
import type { Argv } from 'yargs'

import type { FuseListItem } from 'valaxy/types'
import type { FuseListItem, PostFrontMatter } from 'valaxy/types'
import { matterOptions } from '../plugins/markdown/transform/matter'
import { resolveOptions } from '../options'
import { setEnvProd } from '../utils/env'
Expand All @@ -27,20 +27,21 @@ export async function generateFuseList(options: ResolvedValaxyOptions) {
for await (const i of files) {
const raw = fs.readFileSync(i, 'utf-8')
const { data, excerpt, content } = matter(raw, matterOptions)
const fmData = data as PostFrontMatter

if (data.draft) {
if (fmData.draft) {
consola.warn(`Ignore draft post: ${dim(i)}`)
continue
}

if (data.hide)
if (fmData.hide)
continue

// skip encrypt post
if (data.password)
if (fmData.password)
continue

const keys = options.config.siteConfig.fuse.options.keys || []
const extendKeys = options.config.fuse?.extendKeys || []

// adapt for nested folders, like /posts/2021/01/01/index.md
const relativeLink = i.replace(`${options.userRoot}/pages`, '')
Expand All @@ -49,16 +50,18 @@ export async function generateFuseList(options: ResolvedValaxyOptions) {
: relativeLink.replace(/\.md$/, '')

const fuseListItem: FuseListItem = {
title: data.title || '',
tags: (typeof data.tags === 'string' ? [data.tags] : data.tags) || [],
categories: data.categories || [],
title: fmData.title || '',
tags: (typeof fmData.tags === 'string' ? [fmData.tags] : fmData.tags) || [],
categories: (typeof fmData.categories === 'string' ? [fmData.categories] : fmData.categories) || [],
author: options.config.siteConfig.author.name,
excerpt: excerpt || content.slice(0, 100),
// encode for chinese url
link: encodeURI(link),
}
if (keys.includes('content'))
fuseListItem.content = content || ''

extendKeys.forEach((key) => {
fuseListItem[key] = fmData[key] || ''
})

posts.push(fuseListItem)
}
Expand Down
10 changes: 10 additions & 0 deletions packages/valaxy/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ export interface ValaxyExtendConfig {
icons?: Parameters<typeof presetIcons>[0]
typography?: Parameters<typeof presetTypography>[0]
}
fuse?: {
/**
* @en_US Extends the metadata fields returned by the search
* @zh_CN 扩展搜索返回的元数据字段
* @default []
* @description:en-US By default, returns the following fields: title, tags, categories, author, excerpt, link
* @description:zh-CN 默认返回以下字段:title、tags、categories、author、excerpt、link
*/
extendKeys?: string[]
}
/**
* @experimental
* Enable Vue Devtools & Valaxy Devtools
Expand Down
5 changes: 4 additions & 1 deletion packages/valaxy/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,11 @@ export interface SiteConfig {
*/
options: FuseOptions<FuseListItem> & {
/**
* @en_US The fields to be searched.
* @zh_CN 搜索的字段
* @default ['title', 'tags', 'categories', 'excerpt']
* @description 搜索的字段
* @description:en-US List of keys that will be searched. This supports nested paths, weighted search, and searching in arrays of strings and objects
* @description:zh-CN 搜索将会涉及的字段列表,支持嵌套路径、加权搜索以及在字符串和对象数组中进行搜索
* @see https://fusejs.io/api/options.html#keys
*/
keys: FuseOptions<FuseListItem>['keys']
Expand Down
2 changes: 1 addition & 1 deletion packages/valaxy/types/node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface FuseListItem {
export interface FuseListItem extends Record<string, any> {
title: string
excerpt?: string
author: string
Expand Down

1 comment on commit 55ff977

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Published on https://yun.valaxy.site as production
🚀 Deployed on https://672d6b235e81d6a8f8c412c0--valaxy.netlify.app

Please sign in to comment.