UNPKG

6.47 kBPlain TextView Raw
1<template>
2 <div class="search-box">
3 <input
4 ref="input"
5 aria-label="Search"
6 :value="query"
7 :class="{ 'focused': focused }"
8 :placeholder="placeholder"
9 autocomplete="off"
10 spellcheck="false"
11 @input="query = $event.target.value"
12 @focus="focused = true"
13 @blur="focused = false"
14 @keyup.enter="go(focusIndex)"
15 @keyup.up="onUp"
16 @keyup.down="onDown"
17 >
18 <ul
19 v-if="showSuggestions"
20 class="suggestions"
21 :class="{ 'align-right': alignRight }"
22 @mouseleave="unfocus"
23 >
24 <li
25 v-for="(s, i) in suggestions"
26 :key="i"
27 class="suggestion"
28 :class="{ focused: i === focusIndex }"
29 @mousedown="go(i)"
30 @mouseenter="focus(i)"
31 >
32 <a
33 :href="s.path"
34 @click.prevent
35 >
36 <span class="page-title">{{ s.title || s.path }}</span>
37 <span
38 v-if="s.header"
39 class="header"
40 >&gt; {{ s.header.title }}</span>
41 </a>
42 </li>
43 </ul>
44 </div>
45</template>
46
47<script>
48import matchQuery from './match-query'
49
50/* global SEARCH_MAX_SUGGESTIONS, SEARCH_PATHS, SEARCH_HOTKEYS */
51export default {
52 name: 'SearchBox',
53
54 data () {
55 return {
56 query: '',
57 focused: false,
58 focusIndex: 0,
59 placeholder: undefined
60 }
61 },
62
63 computed: {
64 showSuggestions () {
65 return (
66 this.focused
67 && this.suggestions
68 && this.suggestions.length
69 )
70 },
71
72 suggestions () {
73 const query = this.query.trim().toLowerCase()
74 if (!query) {
75 return
76 }
77
78 const { pages } = this.$site
79 const max = this.$site.themeConfig.searchMaxSuggestions || SEARCH_MAX_SUGGESTIONS
80 const localePath = this.$localePath
81 const res = []
82 for (let i = 0; i < pages.length; i++) {
83 if (res.length >= max) break
84 const p = pages[i]
85 // filter out results that do not match current locale
86 if (this.getPageLocalePath(p) !== localePath) {
87 continue
88 }
89
90 // filter out results that do not match searchable paths
91 if (!this.isSearchable(p)) {
92 continue
93 }
94
95 if (matchQuery(query, p)) {
96 res.push(p)
97 } else if (p.headers) {
98 for (let j = 0; j < p.headers.length; j++) {
99 if (res.length >= max) break
100 const h = p.headers[j]
101 if (h.title && matchQuery(query, p, h.title)) {
102 res.push(Object.assign({}, p, {
103 path: p.path + '#' + h.slug,
104 header: h
105 }))
106 }
107 }
108 }
109 }
110 return res
111 },
112
113 // make suggestions align right when there are not enough items
114 alignRight () {
115 const navCount = (this.$site.themeConfig.nav || []).length
116 const repo = this.$site.repo ? 1 : 0
117 return navCount + repo <= 2
118 }
119 },
120
121 mounted () {
122 this.placeholder = this.$site.themeConfig.searchPlaceholder || ''
123 document.addEventListener('keydown', this.onHotkey)
124 },
125
126 beforeDestroy () {
127 document.removeEventListener('keydown', this.onHotkey)
128 },
129
130 methods: {
131 getPageLocalePath (page) {
132 for (const localePath in this.$site.locales || {}) {
133 if (localePath !== '/' && page.path.indexOf(localePath) === 0) {
134 return localePath
135 }
136 }
137 return '/'
138 },
139
140 isSearchable (page) {
141 let searchPaths = SEARCH_PATHS
142
143 // all paths searchables
144 if (searchPaths === null) { return true }
145
146 searchPaths = Array.isArray(searchPaths) ? searchPaths : new Array(searchPaths)
147
148 return searchPaths.filter(path => {
149 return page.path.match(path)
150 }).length > 0
151 },
152
153 onHotkey (event) {
154 if (event.srcElement === document.body && SEARCH_HOTKEYS.includes(event.key)) {
155 this.$refs.input.focus()
156 event.preventDefault()
157 }
158 },
159
160 onUp () {
161 if (this.showSuggestions) {
162 if (this.focusIndex > 0) {
163 this.focusIndex--
164 } else {
165 this.focusIndex = this.suggestions.length - 1
166 }
167 }
168 },
169
170 onDown () {
171 if (this.showSuggestions) {
172 if (this.focusIndex < this.suggestions.length - 1) {
173 this.focusIndex++
174 } else {
175 this.focusIndex = 0
176 }
177 }
178 },
179
180 go (i) {
181 if (!this.showSuggestions) {
182 return
183 }
184 this.$router.push(this.suggestions[i].path)
185 this.query = ''
186 this.focusIndex = 0
187 },
188
189 focus (i) {
190 this.focusIndex = i
191 },
192
193 unfocus () {
194 this.focusIndex = -1
195 }
196 }
197}
198</script>
199
200<style lang="stylus">
201.search-box
202 display inline-block
203 position relative
204 margin-right 1rem
205 input
206 cursor text
207 width 10rem
208 height: 2rem
209 color lighten($textColor, 25%)
210 display inline-block
211 border 1px solid darken($borderColor, 10%)
212 border-radius 2rem
213 font-size 0.9rem
214 line-height 2rem
215 padding 0 0.5rem 0 2rem
216 outline none
217 transition all .2s ease
218 background #fff url(search.svg) 0.6rem 0.5rem no-repeat
219 background-size 1rem
220 &:focus
221 cursor auto
222 border-color $accentColor
223 .suggestions
224 background #fff
225 width 20rem
226 position absolute
227 top 2 rem
228 border 1px solid darken($borderColor, 10%)
229 border-radius 6px
230 padding 0.4rem
231 list-style-type none
232 &.align-right
233 right 0
234 .suggestion
235 line-height 1.4
236 padding 0.4rem 0.6rem
237 border-radius 4px
238 cursor pointer
239 a
240 white-space normal
241 color lighten($textColor, 35%)
242 .page-title
243 font-weight 600
244 .header
245 font-size 0.9em
246 margin-left 0.25em
247 &.focused
248 background-color #f3f4f5
249 a
250 color $accentColor
251
252@media (max-width: $MQNarrow)
253 .search-box
254 input
255 cursor pointer
256 width 0
257 border-color transparent
258 position relative
259 &:focus
260 cursor text
261 left 0
262 width 10rem
263
264// Match IE11
265@media all and (-ms-high-contrast: none)
266 .search-box input
267 height 2rem
268
269@media (max-width: $MQNarrow) and (min-width: $MQMobile)
270 .search-box
271 .suggestions
272 left 0
273
274@media (max-width: $MQMobile)
275 .search-box
276 margin-right 0
277 input
278 left 1rem
279 .suggestions
280 right 0
281
282@media (max-width: $MQMobileNarrow)
283 .search-box
284 .suggestions
285 width calc(100vw - 4rem)
286 input:focus
287 width 8rem
288</style>