1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
|
var fuse // holds our search engine
var fuseIndex
var searchVisible = false
var firstRun = true // allow us to delay loading json data unless search activated
var list = document.getElementById('searchResults') // targets the <ul>
var first = list.children[0] // first child of search list
var last = list.children[list.children.length - 1] // last child of search list
var maininput = document.getElementById('searchInput') // input box for search
var resultsAvailable = false // Did we get any search results?
// ==========================================
// The main keyboard event listener running the show
//
document.addEventListener('keydown', function (event) {
if (event.altKey && event.which === 191) {
// Load json search index if first time invoking search
// Means we don't load json unless searches are going to happen; keep user payload small unless needed
doSearch(event)
}
// Allow ESC (27) to close search box
if (event.keyCode == 27) {
if (searchVisible) {
document.getElementById('fastSearch').style.visibility = 'hidden'
document.activeElement.blur()
searchVisible = false
}
}
// DOWN (40) arrow
if (event.keyCode == 40) {
if (searchVisible && resultsAvailable) {
console.log('down')
event.preventDefault() // stop window from scrolling
if (document.activeElement == maininput) {
first.focus()
} // if the currently focused element is the main input --> focus the first <li>
else if (document.activeElement == last) {
last.focus()
} // if we're at the bottom, stay there
else {
// console.log(document.activeElement.nextElementSibling)
document.activeElement.nextElementSibling.focus()
} // otherwise select the next search result
}
}
// UP (38) arrow
if (event.keyCode == 38) {
if (searchVisible && resultsAvailable) {
console.log('up')
event.preventDefault() // stop window from scrolling
if (document.activeElement == maininput) {
maininput.focus()
} // If we're in the input box, do nothing
else if (document.activeElement == first) {
maininput.focus()
} // If we're at the first item, go to input box
else {
document.activeElement.previousElementSibling.focus()
} // Otherwise, select the search result above the current active one
}
}
// ENTER (13) key to trigger link click
if (event.keyCode == 13) {
if (searchVisible && resultsAvailable) {
console.log('enter')
// Check if the focused element is a link or inside a link
let focusedElement = document.activeElement
console.log(focusedElement)
if (focusedElement.tagName === 'A') {
focusedElement.click()
} else if (
focusedElement.children[0] &&
focusedElement.children[0].tagName === 'A'
) {
focusedElement.children[0].click()
}
}
}
})
// ==========================================
// execute search as each character is typed
//
document.getElementById('searchInput').onkeyup = function (e) {
executeSearch(this.value)
}
document.querySelector('body').onclick = function (e) {
// console.log(e.target.className);
if (
![
'search-result-item',
'title',
'snippet',
'meta',
'tags',
'categories',
].includes(e.target.className) &&
e.target.tagName !== 'INPUT'
) {
hideSearch()
}
}
document.querySelector('#search-btn').onclick = function (e) {
doSearch(e)
}
function doSearch(e) {
e.stopPropagation()
if (firstRun) {
loadSearch() // loads our json data and builds fuse.js search index
firstRun = false // let's never do this again
}
// Toggle visibility of search box
if (!searchVisible) {
showSearch() // search visible
} else {
hideSearch()
}
}
function hideSearch() {
document.getElementById('fastSearch').style.visibility = 'hidden' // hide search box
document.activeElement.blur() // remove focus from search box
searchVisible = false
}
function showSearch() {
document.getElementById('fastSearch').style.visibility = 'visible' // show search box
document.getElementById('searchInput').focus() // put focus in input box so you can just start typing
searchVisible = true
}
// ==========================================
// fetch some json without jquery
//
function fetchJSONFile(path, callback) {
var httpRequest = new XMLHttpRequest()
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200) {
var data = JSON.parse(httpRequest.responseText)
if (callback) callback(data)
}
}
}
httpRequest.open('GET', path)
httpRequest.send()
}
// ==========================================
// load our search index, only executed once
// on first call of search box (CMD-/)
//
function loadSearch() {
console.log('loadSearch()')
fetchJSONFile('/index.json', function (data) {
var options = {
// fuse.js options; check fuse.js website for details
isCaseSensitive: false, // 是否大小写敏感
shouldSort: true, // 结果集是否按照匹配度排序
includeMatches: true, // 可用于高亮显示匹配字符
threshold: 0.1, // 匹配度阈值,越大越模糊
location: 0,
distance: 500,
maxPatternLength: 30,
minMatchCharLength: 1,
keys: [
{ name: 'content', weight: 0.7 },
// { name: "title", weight: 0.1 },
// { name: "tags", weight: 0.1 },
// { name: "categories", weight: 0.1 },
],
}
// Create the Fuse index
fuseIndex = Fuse.createIndex(options.keys, data)
fuse = new Fuse(data, options, fuseIndex) // build the index from the json file
})
}
// 处理高亮和对内嵌 html 进行转义
function replaceHighlightElements(html) {
return html
.replace(
/<self-defined-mark style="background: ([^&;]+);">/g,
'<self-defined-mark style="background: rgba(102,51,153,0.4); font-size: 14px; font-weight: bold; padding: 2px 5px; border-radius: 2px;">'
)
.replace(/<\/self-defined-mark>/g, '</self-defined-mark>')
}
function escapeHtml(unsafe) {
return replaceHighlightElements(
unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
)
}
// ==========================================
// using the index we loaded on CMD-/, run
// a search query (for "term") every time a letter is typed
// in the search box
//
function executeSearch(term) {
let results = fuse.search(term) // the actual query being run using fuse.js
let searchitems = '' // our results bucket
// console.log(results);
if (results.length === 0) {
// no results based on what was typed into the input box
resultsAvailable = false
searchitems = ''
} else {
// build our html
permalinks = []
numLimit = 15
for (let item in results) {
// only show first 5 results
if (permalinks.length > numLimit) {
break
}
// if (permalinks.includes(results[item].item.permalink)) {
// continue;
// }
// console.log('item: %d, title: %s', item, results[item].item.title)
// 高亮处理函数
const highlightMatches = (text, matches) => {
if (!matches || matches.length === 0) return text
// 将所有匹配位置标记出来
const indices = matches.flatMap((m) => m.indices)
const marked = []
let lastPos = 0
// 按照索引顺序处理文本
indices
.sort((a, b) => a[0] - b[0])
.forEach(([start, end]) => {
// 添加未匹配部分
if (start > lastPos) {
marked.push(text.substring(lastPos, start))
}
// 添加高亮部分
marked.push(
`<self-defined-mark style="background: rgba(102,51,153,0.4);">${text.substring(
start,
end + 1
)}</self-defined-mark>`
)
lastPos = end + 1
})
// 添加剩余文本
if (lastPos < text.length) {
marked.push(text.substring(lastPos))
}
return marked.join('')
}
// 处理内容高亮
let highlightedContent = results[item].item.content
const contentMatches = results[item].matches.filter(
(m) => m.key === 'content'
)
if (contentMatches.length > 0) {
highlightedContent = highlightMatches(
highlightedContent,
contentMatches
)
}
// 处理标题高亮
let highlightedTitle = results[item].item.title
const titleMatches = results[item].matches.filter(
(m) => m.key === 'title'
)
if (titleMatches.length > 0) {
highlightedTitle = highlightMatches(highlightedTitle, titleMatches)
}
searchitems += `
<li class="search-result-item" tabindex="-1">
<a href="${escapeHtml(results[item].item.uri)}" tabindex="0">
<span class="title">${escapeHtml(highlightedTitle)}</span>
<div class="snippet">${escapeHtml(highlightedContent)}</div>
</a>
<div class="meta">
<span class="tags">
<i class="fa fa-tags" aria-hidden="true"></i>
${
results[item].item.tags !== null
? escapeHtml(results[item].item.tags.join('、'))
: ''
}
</span>
<span class="categories">
<i class="far fa-folder" aria-hidden="true"></i>
${
results[item].item.categories !== null
? escapeHtml(results[item].item.categories.join('、'))
: ''
}
</span>
</div>
</li>
`
// console.log(searchitems)
permalinks.push(results[item].item.uri)
}
resultsAvailable = true
}
document.getElementById('searchResults').innerHTML = searchitems
if (results.length > 0) {
first = list.children[0] // first result container — used for checking against keyboard up/down location
last = list.children[list.children.length - 1] // last result container — used for checking against keyboard up/down location
}
}
|