-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5993529
commit 880ecc1
Showing
30 changed files
with
978 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
{ | ||
"name": "@vuepress/plugin-search", | ||
"version": "2.0.0-rc.18", | ||
"description": "VuePress plugin - built-in search", | ||
"keywords": [ | ||
"vuepress-plugin", | ||
"vuepress", | ||
"plugin", | ||
"search" | ||
], | ||
"homepage": "https://ecosystem.vuejs.press/plugins/search.html", | ||
"bugs": { | ||
"url": "https://github.com/vuepress/ecosystem/issues" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/vuepress/ecosystem.git", | ||
"directory": "plugins/plugin-search" | ||
}, | ||
"license": "MIT", | ||
"author": "meteorlxy", | ||
"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.6", | ||
"he": "^1.2.0", | ||
"vue": "^3.4.21" | ||
}, | ||
"peerDependencies": { | ||
"vuepress": "2.0.0-rc.8" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
176 changes: 176 additions & 0 deletions
176
plugins/plugin-flexsearch/src/client/components/SearchBox.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
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 { | ||
useHotKeys, | ||
useSearchIndex, | ||
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 searchIndex = useSearchIndex | ||
|
||
const input = ref<HTMLInputElement | null>(null) | ||
const isActive = ref(false) | ||
const query = ref('') | ||
const locale = computed(() => locales.value[routeLocale.value] ?? {}) | ||
|
||
const suggestions = useSearchSuggestions({ | ||
searchIndex, | ||
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, | ||
}), | ||
], | ||
), | ||
), | ||
), | ||
), | ||
], | ||
) | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './SearchBox.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './useHotKeys.js' | ||
export * from './useSearchIndex.js' | ||
export * from './useSearchSuggestions.js' | ||
export * from './useSuggestionsFocus.js' |
36 changes: 36 additions & 0 deletions
36
plugins/plugin-flexsearch/src/client/composables/useHotKeys.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { onBeforeUnmount, onMounted } from 'vue' | ||
import type { Ref } from 'vue' | ||
import type { HotKeyOptions } from '../../shared/index.js' | ||
import { isFocusingTextControl, isKeyMatched } from '../utils/index.js' | ||
|
||
export const useHotKeys = ({ | ||
input, | ||
hotKeys, | ||
}: { | ||
input: Ref<HTMLInputElement | null> | ||
hotKeys: Ref<(string | HotKeyOptions)[]> | ||
}): void => { | ||
if (hotKeys.value.length === 0) return | ||
|
||
const onKeydown = (event: KeyboardEvent): void => { | ||
if (!input.value) return | ||
if ( | ||
// key matches | ||
isKeyMatched(event, hotKeys.value) && | ||
// event does not come from the search box itself or | ||
// user isn't focusing (and thus perhaps typing in) a text control | ||
!isFocusingTextControl(event.target as EventTarget) | ||
) { | ||
event.preventDefault() | ||
input.value.focus() | ||
} | ||
} | ||
|
||
onMounted(() => { | ||
document.addEventListener('keydown', onKeydown) | ||
}) | ||
|
||
onBeforeUnmount(() => { | ||
document.removeEventListener('keydown', onKeydown) | ||
}) | ||
} |
30 changes: 30 additions & 0 deletions
30
plugins/plugin-flexsearch/src/client/composables/useSearchIndex.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { searchIndex as searchIndexRaw } from '@internal/searchIndex' | ||
import FS from 'flexsearch' | ||
import { ref } from 'vue' | ||
|
||
export interface SearchIndexRet { | ||
path: [string, string] | ||
title: string | ||
} | ||
|
||
export type CSearchIndex = (string, number) => SearchIndexRet[] | ||
|
||
const index = FS.create({ | ||
async: false, | ||
doc: { | ||
id: 'id', | ||
field: ['title', 'content'], | ||
}, | ||
}) | ||
index.import(searchIndexRaw.idx) | ||
|
||
export const useSearchIndex = ref((q: string, c: number) => { | ||
const rr: any = index.search(q, c) | ||
return rr.map((r) => { | ||
return { | ||
path: searchIndexRaw.paths[r.id], | ||
title: r.title, | ||
content: r.content, | ||
} | ||
}) | ||
}) |
81 changes: 81 additions & 0 deletions
81
plugins/plugin-flexsearch/src/client/composables/useSearchSuggestions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { computed } from 'vue' | ||
import type { ComputedRef, Ref } from 'vue' | ||
import { useSearchIndex } from './useSearchIndex.js' | ||
import type { CSearchIndex } from './useSearchIndex.js' | ||
|
||
export interface SearchSuggestion { | ||
link: string | ||
title: string | ||
text: string | ||
} | ||
|
||
function escapeRegExp(string) { | ||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||
} | ||
|
||
function highlightText(fullText, highlightTarget, splitBy) { | ||
let result = fullText | ||
const highlightWords = highlightTarget | ||
.split(splitBy) | ||
.filter((word) => word.length > 0) | ||
if (highlightWords.length > 0) { | ||
for (const word of highlightWords) { | ||
result = result.replace( | ||
new RegExp(escapeRegExp(word), 'ig'), | ||
'<em>$&</em>', | ||
) | ||
} | ||
} else { | ||
result = fullText.replace( | ||
new RegExp(escapeRegExp(highlightTarget), 'ig'), | ||
'<em>$&</em>', | ||
) | ||
} | ||
|
||
return result | ||
} | ||
|
||
function getSuggestionText(content: string, query: string, maxLen: number) { | ||
const queryIndex = content.toLowerCase().indexOf(query.toLowerCase()) | ||
const queryFirstWord = query.split(' ')[0] | ||
let startIndex = | ||
queryIndex === -1 | ||
? content.toLowerCase().indexOf(queryFirstWord.toLowerCase()) | ||
: queryIndex | ||
let prefix = '' | ||
if (startIndex > 15) { | ||
startIndex -= 15 | ||
prefix = '.. ' | ||
} | ||
const text = content.substr(startIndex, maxLen) | ||
return prefix + highlightText(text, query, ' ') | ||
} | ||
|
||
export const useSearchSuggestions = ({ | ||
searchIndex, | ||
routeLocale, | ||
query, | ||
maxSuggestions, | ||
}: { | ||
searchIndex: Ref<CSearchIndex> | ||
routeLocale: Ref<string> | ||
query: Ref<string> | ||
maxSuggestions: Ref<number> | ||
}): ComputedRef<SearchSuggestion[]> => { | ||
return computed(() => { | ||
const searchStr = query.value.trim().toLowerCase() | ||
if (!searchStr) return [] | ||
|
||
const suggestions: SearchSuggestion[] = useSearchIndex | ||
.value(searchStr, maxSuggestions.value) | ||
.map((r) => { | ||
return { | ||
link: r.path, | ||
title: r.title, | ||
text: getSuggestionText(r.content, searchStr, 30), | ||
} | ||
}) | ||
|
||
return suggestions | ||
}) | ||
} |
Oops, something went wrong.