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:
		
							parent
							
								
									d47ad5e88d
								
							
						
					
					
						commit
						eccc40a123
					
				
					 15 changed files with 1094 additions and 9924 deletions
				
			
		
							
								
								
									
										48
									
								
								docs/.vuepress/theme/components/Navbar.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								docs/.vuepress/theme/components/Navbar.vue
									
										
									
									
									
										Normal 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> | ||||
							
								
								
									
										81
									
								
								docs/.vuepress/theme/components/SearchBox.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								docs/.vuepress/theme/components/SearchBox.vue
									
										
									
									
									
										Normal 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> | ||||
							
								
								
									
										6
									
								
								docs/.vuepress/theme/components/search/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/.vuepress/theme/components/search/README.md
									
										
									
									
									
										Normal 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). | ||||
| 
 | ||||
							
								
								
									
										159
									
								
								docs/.vuepress/theme/components/search/SRDSearchBox.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								docs/.vuepress/theme/components/search/SRDSearchBox.vue
									
										
									
									
									
										Normal 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> | ||||
							
								
								
									
										14
									
								
								docs/.vuepress/theme/components/search/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								docs/.vuepress/theme/components/search/index.js
									
										
									
									
									
										Normal 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', '/'] | ||||
|   } | ||||
| }) | ||||
							
								
								
									
										42
									
								
								docs/.vuepress/theme/components/search/match-query.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								docs/.vuepress/theme/components/search/match-query.js
									
										
									
									
									
										Normal 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) | ||||
| } | ||||
| 
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Maxime Moraine
						Maxime Moraine