Skip to content

Commit cb712f9

Browse files
Update Search Form.
1 parent c7cc5e4 commit cb712f9

2 files changed

Lines changed: 178 additions & 86 deletions

File tree

src/app/search.ts

Lines changed: 177 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,71 @@ export interface Query {
99
}
1010

1111
let QueryStorage: Array<Query>
12-
function renderSearchResult(keyword: string, link: string, title: string, text: string) {
12+
function isLiveSearchEnabled(value: unknown) {
13+
if (typeof value === 'boolean') return value;
14+
if (typeof value === 'number') return value === 1;
15+
if (typeof value === 'string') {
16+
const normalized = value.trim().toLowerCase();
17+
return normalized === '1' || normalized === 'true' || normalized === 'on' || normalized === 'yes';
18+
}
19+
return !!value;
20+
}
21+
22+
function renderSearchResult(keyword: string, link: string, title: string, text: string, showPreview = true) {
1323
if (keyword) {
14-
const s = keyword.trim().split(" "),
15-
a = title.indexOf(s[s.length - 1]),
16-
b = text.indexOf(s[s.length - 1]);
17-
title = a < 60 ? title.slice(0, 80) : title.slice(a - 30, a + 30);
18-
title = title.replace(s[s.length - 1], '<mark class="search-keyword">' + s[s.length - 1] + '</mark>');
19-
text = b < 60 ? text.slice(0, 80) : text.slice(b - 30, b + 30);
20-
text = text.replace(s[s.length - 1], '<mark class="search-keyword">' + s[s.length - 1] + '</mark>');
24+
const terms = keyword.trim().split(/\s+/).filter(Boolean);
25+
const lastTerm = terms[terms.length - 1];
26+
const escapedTerm = lastTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27+
const highlightRegExp = new RegExp(escapedTerm, 'ig');
28+
29+
title = title.replace(highlightRegExp, match => `<mark class="search-keyword">${match}</mark>`);
30+
text = text.replace(highlightRegExp, match => `<mark class="search-keyword">${match}</mark>`);
2131
}
32+
const previewHtml = showPreview ? `<p class="ins-search-preview">${text}</p>` : '';
33+
2234
return `<a class="ins-selectable ins-search-item" href="${link}">
2335
<header>${title}</header>
24-
<p class="ins-search-preview">${text}</p>
36+
${previewHtml}
2537
</a>`;
2638
}
2739
function Cx(array: Query[], query: string) {
28-
for (let s = 0; s < query.length; s++) {
29-
if (['.', '?', '*'].indexOf(query[s]) != -1) {
30-
query = query.slice(0, s) + "\\" + query.slice(s);
31-
s++;
32-
}
40+
const terms = query
41+
.trim()
42+
.split(/\s+/)
43+
.filter(Boolean)
44+
.map(term => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
45+
46+
if (!terms.length) {
47+
return [];
3348
}
34-
query = query.replace(query, "^(?=.*?" + query + ").+$").replace(/\s/g, ")(?=.*?");
49+
50+
query = "^" + terms.map(term => `(?=.*${term})`).join('') + ".+$";
51+
const regexp = new RegExp(query, 'i');
52+
3553
return array.filter(
3654
v => Object.values(v)
37-
.some(v => new RegExp(query + '').test(v))
55+
.some(v => regexp.test(String(v ?? '')))
3856
);
3957
}
40-
function query(data: Query[], keyword: string,) {
58+
function query(data: Query[], keyword: string, showPreview = true) {
4159
const sectionStart = '<section class="ins-section"><header class="ins-section-header">';
4260
const sectionEnd = '</section>';
4361
const headerEnd = '</header>';
62+
const normalizedKeyword = keyword.trim();
63+
64+
const resultContainer = document.getElementById("PostlistBox");
65+
const wrapper = resultContainer?.closest<HTMLElement>(".ins-section-wrapper");
66+
if (!resultContainer) {
67+
return;
68+
}
69+
70+
if (!normalizedKeyword) {
71+
resultContainer.innerHTML = '';
72+
if (wrapper) wrapper.style.display = 'none';
73+
return;
74+
}
75+
76+
if (wrapper) wrapper.style.display = '';
4477

4578
let tabBar = document.querySelector<HTMLDivElement>(".ins-tab")!;
4679

@@ -55,32 +88,32 @@ function query(data: Query[], keyword: string,) {
5588
let finalHtml = "";
5689
let tabs = "";
5790

58-
const matchedItems = Cx(data, keyword.trim());
91+
const matchedItems = Cx(data, normalizedKeyword);
5992

6093
for (const item of matchedItems) {
6194
switch (item.type) {
6295
case "post":
63-
articleResults += renderSearchResult(keyword,item.link,item.title,item.text);
96+
articleResults += renderSearchResult(normalizedKeyword, item.link, item.title, item.text, showPreview);
6497
break;
6598

6699
case "shuoshuo":
67-
shuoshuoResults += renderSearchResult(keyword,item.link,item.title,item.text);
100+
shuoshuoResults += renderSearchResult(normalizedKeyword, item.link, item.title, item.text, showPreview);
68101
break;
69102

70103
case "page":
71-
pageResults += renderSearchResult(keyword,item.link,item.title,item.text);
104+
pageResults += renderSearchResult(normalizedKeyword, item.link, item.title, item.text, showPreview);
72105
break;
73106

74107
case "category":
75-
categoryResults += renderSearchResult("",item.link,item.title,item.text);
108+
categoryResults += renderSearchResult("", item.link, item.title, item.text, showPreview);
76109
break;
77110

78111
case "tag":
79-
tagResults += renderSearchResult("",item.link,item.title,"");
112+
tagResults += renderSearchResult("", item.link, item.title, "", showPreview);
80113
break;
81114

82115
case "comment":
83-
commentResults += renderSearchResult(keyword,item.link,item.title,item.text);
116+
commentResults += renderSearchResult(normalizedKeyword, item.link, item.title, item.text, showPreview);
84117
break;
85118
}
86119
}
@@ -110,7 +143,14 @@ function query(data: Query[], keyword: string,) {
110143
finalHtml += '<section class="ins-section type-comment">' + commentResults + sectionEnd;
111144
}
112145

113-
document.getElementById("PostlistBox").innerHTML = '<div class="ins-tab">' + tabs + '</div><div class="ins-type-container">' + finalHtml + "</div>";
146+
if (!tabs || !finalHtml) {
147+
resultContainer.innerHTML = '';
148+
if (wrapper) wrapper.style.display = 'none';
149+
return;
150+
}
151+
152+
resultContainer.innerHTML = '<div class="ins-tab">' + tabs + '</div><div class="ins-type-container">' + finalHtml + "</div>";
153+
if (wrapper) wrapper.style.display = '';
114154

115155
const typeContainer = document.querySelector<HTMLDivElement>(".ins-type-container")!;
116156
tabBar = document.querySelector<HTMLDivElement>(".ins-tab")!;
@@ -196,11 +236,20 @@ function query(data: Query[], keyword: string,) {
196236
};
197237
}
198238

199-
function search_a(val: RequestInfo) {
239+
function search_a(val: RequestInfo, showPreview = true) {
200240
const otxt = (document.getElementById("search-input") as HTMLInputElement)
241+
const resultContainer = document.getElementById("PostlistBox");
242+
if (!resultContainer || !otxt) {
243+
return;
244+
}
245+
201246
if (sessionStorage.getItem('search') != null) {
202-
QueryStorage = JSON.parse(sessionStorage.getItem('search'));
203-
query(QueryStorage, otxt.value, /* Record */);
247+
try {
248+
QueryStorage = JSON.parse(sessionStorage.getItem('search'));
249+
query(QueryStorage, otxt.value, showPreview);
250+
} catch {
251+
sessionStorage.removeItem('search');
252+
}
204253
} else {
205254
fetch(val)
206255
.then(async resp => {
@@ -209,7 +258,7 @@ function search_a(val: RequestInfo) {
209258
if (json != "") {
210259
sessionStorage.setItem('search', json);
211260
QueryStorage = JSON.parse(json);
212-
query(QueryStorage, otxt.value, /* Record */);
261+
query(QueryStorage, otxt.value, showPreview);
213262
}
214263
} else {
215264
console.warn('HTTP ' + resp.status)
@@ -220,70 +269,112 @@ function search_a(val: RequestInfo) {
220269
}
221270

222271
export function SearchDialog() {
223-
let searchButton = document.querySelector(".js-toggle-search") as HTMLElement;
224-
let searchDialog = document.querySelector(".dialog-search-form") as HTMLDialogElement;
225-
let searchForm = document.querySelector(".dialog-search-form form") as HTMLElement;
226-
let detail = document.querySelector(".dialog-search-form .search-detail") as HTMLElement;
227-
228-
if(searchButton && searchDialog){
229-
230-
function closeSearch(){
231-
searchButton.classList.remove('is-active');
232-
searchForm.classList.remove('is-active');
233-
document.documentElement.style.overflowY = 'unset';
234-
searchForm.addEventListener("transitionend",function(){
272+
const searchButton = document.querySelector<HTMLElement>(".js-toggle-search");
273+
const searchDialog = document.querySelector<HTMLDialogElement>(".dialog-search-form");
274+
const searchForm = document.querySelector<HTMLElement>(".dialog-search-form form");
275+
const detail = document.querySelector<HTMLElement>(".dialog-search-form .search-detail");
276+
const closeButton = document.querySelector<HTMLElement>(".dialog-search-form .search-close");
277+
const searchInput = document.getElementById("search-input") as HTMLInputElement;
278+
const resultWrapper = document.querySelector<HTMLElement>(".dialog-search-form .ins-section-wrapper");
279+
280+
if (!searchButton || !searchDialog || !searchForm || !searchInput) {
281+
return;
282+
}
283+
284+
if (searchDialog.dataset.initialized === '1') {
285+
return;
286+
}
287+
searchDialog.dataset.initialized = '1';
288+
289+
const hasResultContainer = !!document.getElementById("PostlistBox");
290+
const canLiveSearch = isLiveSearchEnabled(_iro.live_search) && hasResultContainer;
291+
const canShowPreview = canLiveSearch && isLiveSearchEnabled(_iro.live_search_preview);
292+
293+
if (resultWrapper) {
294+
resultWrapper.style.display = canLiveSearch ? 'none' : '';
295+
}
296+
297+
let lastFocusedElement: HTMLElement | null = null;
298+
299+
function closeSearch() {
300+
if (!searchDialog.open) return;
301+
302+
searchButton.classList.remove('is-active');
303+
searchButton.setAttribute('aria-expanded', 'false');
304+
searchForm.classList.remove('is-active');
305+
document.documentElement.style.overflowY = 'unset';
306+
307+
searchForm.addEventListener("transitionend", function () {
308+
if (searchDialog.open) {
235309
searchDialog.close();
236-
},{once: true})
237-
}
238-
239-
function showSearch(){
240-
searchDialog.showModal();
241-
searchButton.classList.add('is-active');
242-
searchForm.classList.add('is-active');
243-
document.documentElement.style.overflowY = 'hidden';
310+
}
311+
if (lastFocusedElement && document.contains(lastFocusedElement)) {
312+
lastFocusedElement.focus();
313+
}
314+
}, { once: true });
315+
}
316+
317+
function showSearch() {
318+
lastFocusedElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
319+
searchDialog.showModal();
320+
searchButton.classList.add('is-active');
321+
searchButton.setAttribute('aria-expanded', 'true');
322+
searchForm.classList.add('is-active');
323+
document.documentElement.style.overflowY = 'hidden';
324+
window.requestAnimationFrame(() => searchInput.focus());
325+
}
326+
327+
if (canShowPreview && detail) {
328+
detail.addEventListener("click", function () {
329+
const isActive = detail.classList.toggle("active");
330+
searchForm.classList.toggle("show-detail", isActive);
331+
detail.setAttribute('aria-pressed', isActive ? 'true' : 'false');
332+
});
333+
} else {
334+
detail?.remove();
335+
searchForm.classList.remove("show-detail");
336+
}
337+
338+
closeButton?.addEventListener("click", function () {
339+
closeSearch();
340+
});
341+
342+
searchButton.addEventListener("click", function (event) {
343+
event.stopPropagation();
344+
if (searchDialog.open) {
345+
closeSearch();
346+
} else {
347+
showSearch();
244348
}
349+
});
245350

246-
detail.addEventListener("click",function(){
247-
detail.classList.toggle("active");
248-
searchForm.classList.toggle("show-detail");
249-
})
351+
searchDialog.addEventListener('cancel', function (event) {
352+
event.preventDefault();
353+
closeSearch();
354+
});
250355

251-
searchButton.addEventListener("click",function(event){
252-
event.stopPropagation();
253-
if (searchDialog.open){
356+
document.addEventListener("click", function (event) {
357+
const target = event.target;
358+
if (target instanceof Node && !searchForm.contains(target) && !searchButton.contains(target)) {
359+
if (searchDialog.open) {
254360
closeSearch();
255-
} else {
256-
showSearch();
257361
}
258-
})
362+
}
363+
});
259364

260-
document.addEventListener("click",function(event){
261-
let target = event.target;
262-
if(target instanceof Node && !searchForm.contains(target)){
263-
if (searchDialog.open){
264-
closeSearch()
265-
}
365+
if (canLiveSearch) {
366+
QueryStorage = [];
367+
search_a(buildAPI(_iro.api + "sakura/v1/cache_search/json"), canShowPreview);
368+
369+
let searchFlag: ReturnType<typeof setTimeout> = null;
370+
searchInput.oninput = function () {
371+
if (searchFlag != null) {
372+
clearTimeout(searchFlag);
266373
}
267-
})
268-
269-
if (_iro.live_search) {
270-
QueryStorage = [];
271-
search_a(buildAPI(_iro.api + "sakura/v1/cache_search/json"));
272-
273-
let otxt = document.getElementById("search-input") as HTMLInputElement,
274-
//list = document.getElementById("PostlistBox"),
275-
//Record = list.innerHTML,
276-
searchFlag: ReturnType<typeof setTimeout> = null;
277-
otxt.oninput = function () {
278-
if (searchFlag != null) {
279-
clearTimeout(searchFlag);
280-
}
281-
searchFlag = setTimeout(function () {
282-
query(QueryStorage, otxt.value, /* Record */);
283-
}, 250);
284-
};
285-
document.addEventListener("pjax:complete",closeSearch);
286-
}
287-
374+
searchFlag = setTimeout(function () {
375+
query(QueryStorage || [], searchInput.value, canShowPreview);
376+
}, 250);
377+
};
378+
document.addEventListener("pjax:complete", closeSearch);
288379
}
289380
}

src/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ declare namespace _iro {
5555
const jsdelivr_css_src: string
5656
const land_at_home: boolean
5757
const live_search: boolean
58+
const live_search_preview: boolean
5859
/**
5960
* 文章特色图取色
6061
*/

0 commit comments

Comments
 (0)