1
0
Fork 0
mirror of https://github.com/em-squared/5e-drs.git synced 2025-10-31 13:34:21 +00:00

search autocomplete

This commit is contained in:
Maxime Moraine 2020-03-28 10:42:54 +01:00
parent d47ad5e88d
commit eccc40a123
15 changed files with 1094 additions and 9924 deletions

View file

@ -0,0 +1,48 @@
<template>
<v-app-bar :clipped-left="$vuetify.breakpoint.lgAndUp" app color="blue darken-3" dark>
<v-app-bar-nav-icon @click.stop="setDrawer" />
<v-toolbar-title class="ml-0 mr-4 pl-4">
<v-btn class="hidden-sm-and-down site-title" text link :to="{ path: '/' }">{{ $site.title }}</v-btn>
</v-toolbar-title>
<SRDSearchBox v-if="$site.themeConfig.search !== false && $page.frontmatter.search !== false" />
<!-- <v-text-field flat solo-inverted hide-details prepend-inner-icon="mdi-magnify" label="Search" class="hidden-sm-and-down" /> -->
<v-spacer />
</v-app-bar>
</template>
<script>
import SRDSearchBox from '@theme/components/search/SRDSearchBox.vue'
// import NavLinks from '@theme/components/NavLinks.vue'
export default {
name: 'Navbar',
components: {
// NavLinks,
SRDSearchBox
},
data () {
return {
}
},
computed: {
drawer() {
return this.$store.state.drawer
}
},
methods: {
setDrawer () {
this.$store.commit('setDrawer', !this.$store.state.drawer)
}
}
}
</script>
<style lang="scss">
.site-title.theme--dark.v-btn--active:before {
opacity: 0;
}
</style>

View file

@ -0,0 +1,81 @@
<template>
<v-autocomplete
v-model="select"
:loading="loading"
:items="items"
:search-input.sync="search"
cache-items
class="mx-4"
flat
clearable
hide-no-data
hide-details
item-text="title"
item-value="path"
:label="$site.themeConfig.searchPlaceholder"
append-icon="mdi-magnify"
solo-inverted
></v-autocomplete>
</template>
<script>
import Flexsearch from "flexsearch";
export default {
data () {
return {
index: null,
loading: false,
items: [],
search: null,
select: null,
results: [{}]
}
},
watch: {
search (val) {
val && val.length > 1 && val !== this.select && this.querySelections(val)
},
select (val) {
if (val) {
this.$router.push(val).catch(err => {})
}
}
},
mounted () {
this.index = new Flexsearch({
tokenize: "forward",
doc: {
id: "key",
field: ["title", "headers"]
}
})
const { pages } = this.$site
console.log(pages)
this.index.add(pages)
},
methods: {
querySelections (v) {
this.loading = true
this.index.search(
v,
{
limit: 10,
threshold: 2,
encode: 'extra'
},
(result) => {
console.log(result)
this.items = result
this.loading = false
})
},
},
}
</script>

View file

@ -0,0 +1,6 @@
# @vuepress/plugin-search
> header-based search plugin for VuePress
See [documentation](https://v1.vuepress.vuejs.org/plugin/official/plugin-search.html).

View file

@ -0,0 +1,159 @@
<template>
<v-autocomplete
v-model="select"
:loading="loading"
:items="entries"
:search-input.sync="search"
cache-items
class="mx-4"
flat
clearable
hide-no-data
hide-details
item-text="title"
item-value="path"
:label="placeholder"
append-icon="mdi-magnify"
solo-inverted
return-object
>
<template v-slot:item="data">
<template>
<v-list-item-content>
<v-list-item-title v-html="data.item.title"></v-list-item-title>
<v-list-item-subtitle v-if="data.item.subtitle" v-html="data.item.subtitle"></v-list-item-subtitle>
</v-list-item-content>
</template>
</template>
</v-autocomplete>
</template>
<script>
import matchQuery from './match-query'
/* global SEARCH_MAX_SUGGESTIONS, SEARCH_PATHS, SEARCH_HOTKEYS */
export default {
name: 'SRDSearchBox',
data () {
return {
loading: false,
focused: false,
focusIndex: 0,
placeholder: undefined,
search: null,
select: null,
items: []
}
},
watch: {
search (query) {
if (query && query.length > 1) {
this.suggestions(query)
}
},
select (selected) {
this.go(selected)
}
},
computed: {
entries () {
return this.items.map(item => {
return {
title: item.title,
subtitle: item.subtitle,
path: item.path
}
})
}
},
mounted () {
this.placeholder = this.$site.themeConfig.searchPlaceholder || ''
},
methods: {
suggestions (query) {
this.loading = true
query = query.trim().toLowerCase()
if (!query) {
this.items = []
return
}
const { pages } = this.$site
const max = this.$site.themeConfig.searchMaxSuggestions || 10
const localePath = this.$localePath
const res = []
for (let i = 0; i < pages.length; i++) {
if (res.length >= max) break
const p = pages[i]
// filter out results that do not match current locale
if (this.getPageLocalePath(p) !== localePath) {
continue
}
// filter out results that do not match searchable paths
if (!this.isSearchable(p)) {
continue
}
if (matchQuery(query, p)) {
res.push(p)
} else if (p.headers) {
for (let j = 0; j < p.headers.length; j++) {
if (res.length >= max) break
const h = p.headers[j]
if (h.title && matchQuery(query, p, h.title)) {
res.push(Object.assign({}, p, {
subtitle: p.title,
title: h.title,
path: p.path + '#' + h.slug,
header: h
}))
}
}
}
}
this.items = res
this.loading = false
},
getPageLocalePath (page) {
for (const localePath in this.$site.locales || {}) {
if (localePath !== '/' && page.path.indexOf(localePath) === 0) {
return localePath
}
}
return '/'
},
isSearchable (page) {
let searchPaths = null
// all paths searchables
if (searchPaths === null) { return true }
searchPaths = Array.isArray(searchPaths) ? searchPaths : new Array(searchPaths)
return searchPaths.filter(path => {
return page.path.match(path)
}).length > 0
},
go (selected) {
if (selected) {
this.$router.push(selected.path)
}
}
}
}
</script>
<style>
</style>

View file

@ -0,0 +1,14 @@
const { path } = require('@vuepress/shared-utils')
module.exports = (options) => ({
alias: {
'@SRDSearchBox':
path.resolve(__dirname, 'SearchBox.vue')
},
define: {
SEARCH_MAX_SUGGESTIONS: options.searchMaxSuggestions || 5,
SEARCH_PATHS: options.test || null,
SEARCH_HOTKEYS: options.searchHotkeys || ['s', '/']
}
})

View file

@ -0,0 +1,42 @@
import get from 'lodash/get'
export default (query, page, additionalStr = null) => {
let domain = get(page, 'title', '')
if (get(page, 'frontmatter.tags')) {
domain += ` ${page.frontmatter.tags.join(' ')}`
}
if (additionalStr) {
domain += ` ${additionalStr}`
}
return matchTest(query, domain)
}
const matchTest = (query, domain) => {
const escapeRegExp = str => str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
const words = query
.split(/\s+/g)
.map(str => str.trim())
.filter(str => !!str)
const hasTrailingSpace = query.endsWith(' ')
const searchRegex = new RegExp(
words
.map((word, index) => {
if (words.length === index + 1 && !hasTrailingSpace) {
// The last word - ok with the word being "startswith"-like
return `(?=.*\\b${escapeRegExp(word)})`
} else {
// Not the last word - expect the whole word exactly
return `(?=.*\\b${escapeRegExp(word)}\\b)`
}
})
.join('') + '.+',
'gi'
)
return searchRegex.test(domain)
}