Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: init flexsearch plugin #91

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions docs/plugins/flexsearch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# flexsearch

<NpmBadge package="@vuepress/plugin-flexsearch" />

Provide local search to your documentation site.

Unlike the `search` plugin, the `flexsearch` plugin uses the [FlexSearch](https://github.com/nextapps-de/flexsearch) library and pre-indexes both the title and the content of the pages.

::: tip
Default theme will add search box to the navbar once you configure this plugin correctly.

This plugin may not be used directly in other themes, so you'd better refer to the documentation of your theme for more details.
:::

## Usage

```bash
npm i -D @vuepress/plugin-flexsearch@next
```

```ts
import { flexsearchPlugin } from '@vuepress/plugin-flexsearch'

export default {
plugins: [
flexsearchPlugin({
// options
}),
],
}
```

## Local Search Index

This plugin will generate search index from your pages locally, and load the search index file when users enter your site. In other words, this is a lightweight built-in search which does not require any external requests.

While this plugin should be able to handle more pages than the `search` plugin, as it is using an index, the limit of how many pages it can support has not been tested yet. Please refer to [FlexSearch](https://github.com/nextapps-de/flexsearch) benchmarks for more detailed info.

Another alternative to this plugin is to use a more professional solution - [docsearch](./docsearch.md).

## Options

### locales

- Type: `Record<string, { placeholder: string }>`

- Details:

The text of the search box in different locales.

If this option is not specified, it will fallback to default text.

- Example:

```ts
export default {
plugins: [
flexsearchPlugin({
locales: {
'/': {
placeholder: 'Search',
},
'/zh/': {
placeholder: '搜索',
},
},
}),
],
}
```

- Also see:
- [Guide > I18n](https://vuejs.press/guide/i18n.html)

### hotKeys

- Type: `(string | HotKeyOptions)[]`

@[code ts](@vuepress/plugin-flexsearch/src/shared/hotKey.ts)

- Default: `['s', '/']`

- Details:

Specify the [event.key](http://keycode.info/) of the hotkeys.

When hotkeys are pressed, the search box input will be focused.

Set to an empty array to disable hotkeys.

### maxSuggestions

- Type: `number`

- Default: `5`

- Details:

Specify the maximum number of search results.

### isSearchable

- Type: `(page: Page) => boolean`

- Default: `() => true`

- Details:

A function to determine whether a page should be included in the search index.

- Return `true` to include the page.
- Return `false` to exclude the page.

- Example:

```ts
export default {
plugins: [
flexsearchPlugin({
// exclude the homepage
isSearchable: (page) => page.path !== '/',
}),
],
}
```

## Styles

You can customize the style of the search box via CSS variables:

@[code css](@vuepress/plugin-flexsearch/src/client/styles/vars.css)

## Components

### SearchBox

- Details:

This plugin will register a `<SearchBox />` component globally, and you can use it without any props.

Put this component to where you want to place the search box. For example, default theme puts this component to the end of the navbar.

::: tip
This component is mainly used for theme development. You don't need to use it directly in most cases.
:::
4 changes: 4 additions & 0 deletions plugins/plugin-flexsearch/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
51 changes: 51 additions & 0 deletions plugins/plugin-flexsearch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@vuepress/plugin-flexsearch",
"version": "2.0.0-rc.21",
"description": "VuePress plugin - built-in search using flexsearch",
"keywords": [
"vuepress-plugin",
"vuepress",
"plugin",
"search"
],
"homepage": "https://ecosystem.vuejs.press/plugins/flexsearch.html",
"bugs": {
"url": "https://github.com/vuepress/ecosystem/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuepress/ecosystem.git",
"directory": "plugins/plugin-flexsearch"
},
"license": "MIT",
"author": "svalaskevicius",
"type": "module",
"exports": {
".": "./lib/node/index.js",
"./client": "./lib/client/index.js",
"./package.json": "./package.json"
},
"main": "./lib/node/index.js",
"types": "./lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "tsc -b tsconfig.build.json",
"clean": "rimraf --glob ./lib ./*.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,svg}\" lib",
"style": "sass src:lib --no-source-map"
},
"dependencies": {
"chokidar": "^3.6.0",
"flexsearch": "^0.7.43",
"he": "^1.2.0",
"vue": "^3.4.21"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.9"
},
"publishConfig": {
"access": "public"
}
}
175 changes: 175 additions & 0 deletions plugins/plugin-flexsearch/src/client/components/SearchBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { computed, defineComponent, h, ref, toRefs } from 'vue'
import type { PropType } from 'vue'
import { useRouteLocale, useRouter } from 'vuepress/client'
import type { LocaleConfig } from 'vuepress/shared'
import type { HotKeyOptions } from '../../shared/index.js'
import {
findInIndex,
useHotKeys,
useSearchSuggestions,
useSuggestionsFocus,
} from '../composables/index.js'

export type SearchBoxLocales = LocaleConfig<{
placeholder: string
}>

export const SearchBox = defineComponent({
name: 'SearchBox',

props: {
locales: {
type: Object as PropType<SearchBoxLocales>,
required: false,
default: () => ({}),
},

hotKeys: {
type: Array as PropType<(string | HotKeyOptions)[]>,
required: false,
default: () => [],
},

maxSuggestions: {
type: Number,
required: false,
default: 5,
},
},

setup(props) {
const { locales, hotKeys, maxSuggestions } = toRefs(props)

const router = useRouter()
const routeLocale = useRouteLocale()

const input = ref<HTMLInputElement | null>(null)
const isActive = ref(false)
const query = ref('')
const locale = computed(() => locales.value[routeLocale.value] ?? {})

const suggestions = useSearchSuggestions({
findInIndex,
routeLocale,
query,
maxSuggestions,
})
const { focusIndex, focusNext, focusPrev } =
useSuggestionsFocus(suggestions)
useHotKeys({ input, hotKeys })

const showSuggestions = computed(
() => isActive.value && !!suggestions.value.length,
)
const onArrowUp = (): void => {
if (!showSuggestions.value) {
return
}
focusPrev()
}
const onArrowDown = (): void => {
if (!showSuggestions.value) {
return
}
focusNext()
}
const goTo = (index: number): void => {
if (!showSuggestions.value) {
return
}

const suggestion = suggestions.value[index]
if (!suggestion) {
return
}

router.push(suggestion.link).then(() => {
query.value = ''
focusIndex.value = 0
})
}

return () =>
h(
'form',
{
class: 'search-box',
role: 'search',
},
[
h('input', {
ref: input,
type: 'search',
placeholder: locale.value.placeholder,
autocomplete: 'off',
spellcheck: false,
value: query.value,
onFocus: () => (isActive.value = true),
onBlur: () => (isActive.value = false),
onInput: (event) =>
(query.value = (event.target as HTMLInputElement).value),
onKeydown: (event) => {
switch (event.key) {
case 'ArrowUp': {
onArrowUp()
break
}
case 'ArrowDown': {
onArrowDown()
break
}
case 'Enter': {
event.preventDefault()
goTo(focusIndex.value)
break
}
}
},
}),
showSuggestions.value &&
h(
'ul',
{
class: 'suggestions',
onMouseleave: () => (focusIndex.value = -1),
},
suggestions.value.map(({ link, title, text }, index) =>
h(
'li',
{
class: [
'suggestion',
{
focus: focusIndex.value === index,
},
],
onMouseenter: () => (focusIndex.value = index),
onMousedown: () => goTo(index),
},
h(
'a',
{
href: link,
onClick: (event) => event.preventDefault(),
},
[
h(
'span',
{
class: 'page-title',
},
title,
),
h('span', {
class: 'suggestion-result',
innerHTML: text,
}),
],
),
),
),
),
],
)
},
})
1 change: 1 addition & 0 deletions plugins/plugin-flexsearch/src/client/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './SearchBox.js'
Loading
Loading