mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-11-02 20:33:42 +00:00
Update search.js
搜索结果中标出搜索词;上下键在结果间移动、回车打开、Esc 先取消选中再清空,⌘K / Ctrl+K 聚焦搜索框;打开结果后在目标小节中标出搜索词。
This commit is contained in:
+144
-15
@@ -77,21 +77,37 @@
|
||||
});
|
||||
}
|
||||
|
||||
function snippet(text, query) {
|
||||
var first = normalize(query).split(' ')[0];
|
||||
var position = normalize(text).indexOf(first);
|
||||
function snippet(text, terms) {
|
||||
var lower = normalize(text), position = -1;
|
||||
// 先找最长的词:“死锁的四个条件”的摘要应定位到“四个条件”,而不是第一次出现“死锁”的地方
|
||||
(Array.isArray(terms) ? terms.slice() : normalize(terms).split(' ')).sort(function (a, b) { return b.length - a.length; })
|
||||
.some(function (term) { position = term ? lower.indexOf(term) : -1; return position !== -1; });
|
||||
var start = Math.max(0, position - 45);
|
||||
return (start ? '…' : '') + text.slice(start, start + 170) + (text.length > start + 170 ? '…' : '');
|
||||
}
|
||||
|
||||
// 摘要和标题中标出搜索词:先按搜索词切分原文再逐段转义,不会匹配到转义后的实体
|
||||
function highlight(text, query) {
|
||||
var terms = (Array.isArray(query) ? query : normalize(query).split(' ')).filter(Boolean).sort(function (a, b) { return b.length - a.length; }).map(function (term) {
|
||||
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
});
|
||||
if (!terms.length) return escapeHtml(text);
|
||||
var pattern = new RegExp('(' + terms.join('|') + ')', 'gi');
|
||||
return String(text).split(pattern).map(function (part, index) {
|
||||
return index % 2 ? '<mark>' + escapeHtml(part) + '</mark>' : escapeHtml(part);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function mountSearch() {
|
||||
var sidebar = document.querySelector('.sidebar');
|
||||
if (!sidebar || sidebar.querySelector('.site-search')) return;
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'search site-search';
|
||||
wrapper.innerHTML = '<input type="search" aria-label="搜索全部笔记" aria-controls="site-search-results" placeholder="搜索全部笔记" autocomplete="off">' +
|
||||
var mac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
||||
wrapper.innerHTML = '<input type="search" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-label="搜索全部笔记" aria-controls="site-search-results" placeholder="搜索全部笔记" autocomplete="off" enterkeyhint="search">' +
|
||||
'<kbd class="search-key" aria-hidden="true">' + (mac ? '⌘K' : 'Ctrl K') + '</kbd>' +
|
||||
'<p class="search-status" role="status" aria-live="polite"></p>' +
|
||||
'<div id="site-search-results" class="results-panel" hidden></div>';
|
||||
'<div id="site-search-results" class="results-panel" role="listbox" aria-label="搜索结果" hidden></div>';
|
||||
sidebar.insertBefore(wrapper, sidebar.firstChild);
|
||||
var input = wrapper.querySelector('input');
|
||||
var status = wrapper.querySelector('.search-status');
|
||||
@@ -104,40 +120,153 @@
|
||||
var request = ++sequence;
|
||||
panel.hidden = !query;
|
||||
panel.innerHTML = '';
|
||||
input.setAttribute('aria-expanded', String(!!query));
|
||||
setActive(-1);
|
||||
if (!query) { status.textContent = ''; return; }
|
||||
status.textContent = '正在搜索…';
|
||||
loadIndex().then(function (index) {
|
||||
if (request !== sequence) return;
|
||||
var results = search(index, query);
|
||||
status.textContent = results.length ? '找到 ' + results.length + ' 个相关章节' : '没有找到相关内容';
|
||||
panel.innerHTML = results.map(function (result) {
|
||||
var results = search(index, query), terms = normalize(query).split(' ').filter(Boolean);
|
||||
status.textContent = results.length ? '找到 ' + results.length + ' 个相关章节' : '没有找到相关内容,可以换个说法或减少关键词';
|
||||
lastTerms = terms;
|
||||
panel.innerHTML = results.map(function (result, i) {
|
||||
var url = '/#' + result.path.split('/').map(encodeURIComponent).join('/') +
|
||||
(result.id ? '?id=' + encodeURIComponent(result.id) : '');
|
||||
return '<a class="matching-post" href="' + escapeHtml(url) + '"><strong>' + escapeHtml(result.title) +
|
||||
'</strong><small>' + escapeHtml(result.page) + '</small><p>' + escapeHtml(snippet(result.text, query)) + '</p></a>';
|
||||
return '<a class="matching-post" id="search-result-' + i + '" role="option" aria-selected="false" tabindex="-1" href="' + escapeHtml(url) + '"><strong>' + highlight(result.title, terms) +
|
||||
'</strong><small>' + escapeHtml(result.page) + '</small><p>' + highlight(snippet(result.text, terms), terms) + '</p></a>';
|
||||
}).join('');
|
||||
markCurrent();
|
||||
}).catch(function () {
|
||||
if (request === sequence) status.textContent = '搜索暂时不可用,请稍后重试。';
|
||||
});
|
||||
}
|
||||
input.addEventListener('focus', function () { loadIndex().catch(function () {}); });
|
||||
input.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(update, 140); });
|
||||
// 当前选中的结果:焦点一直在输入框里,可以接着修改搜索词
|
||||
var active = -1, lastTerms = [];
|
||||
function options() { return Array.from(panel.querySelectorAll('.matching-post')); }
|
||||
function setActive(i) {
|
||||
var list = options();
|
||||
active = list.length ? Math.max(-1, Math.min(i, list.length - 1)) : -1;
|
||||
list.forEach(function (link, j) { link.setAttribute('aria-selected', String(j === active)); });
|
||||
if (active >= 0) {
|
||||
input.setAttribute('aria-activedescendant', list[active].id);
|
||||
list[active].scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
input.removeAttribute('aria-activedescendant');
|
||||
}
|
||||
}
|
||||
input.addEventListener('keydown', function (event) {
|
||||
if (event.isComposing || event.keyCode === 229) return;
|
||||
if (event.key === 'Escape') { input.value = ''; update(); }
|
||||
var first = panel.querySelector('a');
|
||||
if (first && event.key === 'ArrowDown') { event.preventDefault(); first.focus(); }
|
||||
if (first && event.key === 'Enter') { event.preventDefault(); first.click(); }
|
||||
var list = options();
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
if (!list.length) return;
|
||||
event.preventDefault();
|
||||
var step = event.key === 'ArrowDown' ? 1 : -1;
|
||||
setActive(active === -1 && step < 0 ? list.length - 1 : active + step >= list.length ? 0 : active + step);
|
||||
} else if (event.key === 'Enter') {
|
||||
// 结果还没出来时(输入后 140ms 内)先搜一次,再打开第一条
|
||||
if (!list.length && input.value.trim()) { clearTimeout(timer); update(); return; }
|
||||
if (!list.length) return;
|
||||
event.preventDefault();
|
||||
(list[active] || list[0]).click();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
if (active >= 0) setActive(-1);
|
||||
else if (input.value) { input.value = ''; update(); }
|
||||
else input.blur();
|
||||
}
|
||||
});
|
||||
panel.addEventListener('mousemove', function (event) {
|
||||
var link = event.target.closest('.matching-post');
|
||||
if (link) { var i = options().indexOf(link); if (i !== active) setActive(i); }
|
||||
});
|
||||
// ⌘K / Ctrl+K 在任何地方聚焦搜索框(手机上同时打开侧边栏)
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key !== 'k' && event.key !== 'K' || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('close', window.matchMedia('(max-width: 768px)').matches);
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
// 搜索结果在跳转后保留,方便接着看下一条;当前所在的小节标为 aria-current
|
||||
function markCurrent() {
|
||||
var here = decodeURIComponent(location.hash);
|
||||
panel.querySelectorAll('.matching-post').forEach(function (link) {
|
||||
var same = decodeURIComponent(link.getAttribute('href').slice(1)) === here;
|
||||
if (same) link.setAttribute('aria-current', 'location'); else link.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
window.addEventListener('hashchange', markCurrent);
|
||||
panel.addEventListener('click', function (event) {
|
||||
if (event.target.closest('a') && window.matchMedia('(max-width: 768px)').matches) {
|
||||
var link = event.target.closest('a');
|
||||
if (link) {
|
||||
try { sessionStorage.setItem('cs-notes-search-hit', JSON.stringify({ href: link.getAttribute('href'), terms: lastTerms, time: Date.now() })); } catch (error) {}
|
||||
setTimeout(markHits, 60);
|
||||
}
|
||||
if (link && window.matchMedia('(max-width: 768px)').matches) {
|
||||
document.body.classList.remove('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var HIT_SKIP = /^(PRE|CODE|SCRIPT|STYLE|MARK|BUTTON|SVG)$/;
|
||||
function clearHits() {
|
||||
document.querySelectorAll('mark.search-hit').forEach(function (mark) {
|
||||
mark.replaceWith(document.createTextNode(mark.textContent));
|
||||
});
|
||||
}
|
||||
function markHits() {
|
||||
var saved;
|
||||
try { saved = JSON.parse(sessionStorage.getItem('cs-notes-search-hit')); } catch (error) { return; }
|
||||
if (!saved || Date.now() - saved.time > 30000) return;
|
||||
var hash = decodeURIComponent(location.hash), href = decodeURIComponent(saved.href.replace(/^\//, ''));
|
||||
if (hash !== href) return;
|
||||
sessionStorage.removeItem('cs-notes-search-hit');
|
||||
clearHits();
|
||||
var id = new URLSearchParams(href.split('?').slice(1).join('?')).get('id');
|
||||
var heading = id && document.getElementById(id), section = document.querySelector('.markdown-section');
|
||||
if (!section || !saved.terms || !saved.terms.length) return;
|
||||
var level = heading && /^H([1-6])$/.test(heading.tagName) ? Number(heading.tagName[1]) : 0;
|
||||
var nodes = [], node = heading || section.firstElementChild;
|
||||
for (; node; node = node.nextElementSibling) {
|
||||
if (node !== heading && /^H[1-6]$/.test(node.tagName) && Number(node.tagName[1]) <= level) break;
|
||||
nodes.push(node);
|
||||
}
|
||||
var terms = saved.terms.filter(Boolean).sort(function (a, b) { return b.length - a.length; });
|
||||
var pattern = new RegExp('(' + terms.map(function (term) { return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }).join('|') + ')', 'gi');
|
||||
var count = 0;
|
||||
nodes.forEach(function (block) {
|
||||
var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: function (text) {
|
||||
for (var p = text.parentNode; p && p !== block.parentNode; p = p.parentNode) if (HIT_SKIP.test(p.nodeName.toUpperCase())) return NodeFilter.FILTER_REJECT;
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
var texts = [];
|
||||
while (walker.nextNode()) texts.push(walker.currentNode);
|
||||
texts.forEach(function (text) {
|
||||
var parts = text.nodeValue.split(pattern);
|
||||
if (parts.length < 2 || count > 60) return;
|
||||
var fragment = document.createDocumentFragment();
|
||||
parts.forEach(function (part, i) {
|
||||
if (!part) return;
|
||||
if (i % 2) { var mark = document.createElement('mark'); mark.className = 'search-hit'; mark.textContent = part; fragment.appendChild(mark); count++; }
|
||||
else fragment.appendChild(document.createTextNode(part));
|
||||
});
|
||||
text.replaceWith(fragment);
|
||||
});
|
||||
});
|
||||
if (heading) {
|
||||
heading.classList.remove('jump-target');
|
||||
void heading.offsetWidth;
|
||||
heading.classList.add('jump-target');
|
||||
}
|
||||
}
|
||||
|
||||
window.$docsify.plugins.push(function (hook) {
|
||||
hook.mounted(mountSearch);
|
||||
hook.doneEach(mountSearch);
|
||||
hook.doneEach(function () { setTimeout(markHits, 0); });
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user