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 | >> {{ s.header.title }}</span>
|
41 | </a>
|
42 | </li>
|
43 | </ul>
|
44 | </div>
|
45 | </template>
|
46 |
|
47 | <script>
|
48 | import matchQuery from './match-query'
|
49 |
|
50 |
|
51 | export 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 |
|
86 | if (this.getPageLocalePath(p) !== localePath) {
|
87 | continue
|
88 | }
|
89 |
|
90 |
|
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 |
|
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 |
|
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>
|