Improve breadcrumb and code block controls

This commit is contained in:
2026-05-29 15:08:44 +08:00
parent cf2d5fc77b
commit eb73d41d35
7 changed files with 465 additions and 54 deletions
+147 -1
View File
@@ -13,6 +13,7 @@
const btnToggleSidebar = document.getElementById('btnToggleSidebar');
const btnToggleTheme = document.getElementById('btnToggleTheme');
const themeSelect = document.getElementById('themeSelect');
const toolbarBreadcrumb = document.getElementById('toolbarBreadcrumb');
const contentBody = document.getElementById('contentBody');
const emptyState = document.getElementById('emptyState');
const markdownContent = document.getElementById('markdownContent');
@@ -23,11 +24,13 @@
const tocPanel = document.getElementById('tocPanel');
const loadingBar = document.getElementById('loadingBar');
const sidebar = document.getElementById('sidebar');
const app = document.getElementById('app');
const mobileNavBtn = document.getElementById('mobileNavBtn');
const themeStylesheet = document.getElementById('themeStylesheet');
const themeHighlightStylesheet = document.getElementById('themeHighlightStylesheet');
let currentFile = null;
const titleByPath = buildTitleIndex(navTree);
const fallbackTheme = themes[0] || {
id: 'default',
label: 'Default',
@@ -115,10 +118,12 @@
function setSidebarVisible(visible) {
if (visible) {
sidebar.classList.remove('collapsed');
app.classList.remove('sidebar-collapsed');
localStorage.setItem('webook-sidebar', 'visible');
btnToggleSidebar.textContent = '☰';
} else {
sidebar.classList.add('collapsed');
app.classList.add('sidebar-collapsed');
localStorage.setItem('webook-sidebar', 'hidden');
btnToggleSidebar.textContent = '☷';
}
@@ -197,6 +202,63 @@
return div.innerHTML;
}
function buildTitleIndex(items, index = {}) {
if (!Array.isArray(items)) return index;
items.forEach((item) => {
if (item.type === 'file') {
const title = item.meta && item.meta.title
? item.meta.title
: item.name.replace(/\.(md|markdown)$/i, '');
index[item.path] = title;
return;
}
if (item.type === 'dir' && Array.isArray(item.children)) {
buildTitleIndex(item.children, index);
}
});
return index;
}
function getFileDisplayTitle(filePath, preferredTitle) {
if (preferredTitle) return preferredTitle;
if (titleByPath[filePath]) return titleByPath[filePath];
if (!filePath) return '';
const fileName = filePath.split('/').pop() || filePath;
return fileName.replace(/\.(md|markdown)$/i, '');
}
function renderToolbarBreadcrumb(filePath, preferredTitle) {
if (!toolbarBreadcrumb) return;
const segments = ['<span class="toolbar-kicker">Docs</span>'];
const parts = typeof filePath === 'string' && filePath
? filePath.split('/').filter(Boolean)
: [];
if (!parts.length) {
toolbarBreadcrumb.innerHTML = segments.join('');
return;
}
const directoryParts = parts.slice(0, -1);
const fileTitle = getFileDisplayTitle(filePath, preferredTitle);
directoryParts.forEach((part) => {
segments.push('<span class="toolbar-divider">/</span>');
segments.push(`<span class="toolbar-segment">${escapeHtml(part)}</span>`);
});
if (fileTitle) {
segments.push('<span class="toolbar-divider">/</span>');
segments.push(`<span class="toolbar-segment is-current">${escapeHtml(fileTitle)}</span>`);
}
toolbarBreadcrumb.innerHTML = segments.join('');
}
// Render nav
sidebarNav.innerHTML = renderNavTree(navTree);
@@ -308,7 +370,6 @@
async function loadFile(filePath) {
if (currentFile === filePath) return;
currentFile = filePath;
showLoading();
try {
@@ -325,11 +386,14 @@
emptyState.style.display = 'none';
markdownContent.style.display = '';
currentFile = filePath;
articleTitle.textContent = data.title;
articleBody.innerHTML = data.html;
enhanceCodeBlocks();
buildTOC();
renderArticleMeta(data.meta);
renderToolbarBreadcrumb(filePath, data.title);
// Scroll to top
contentBody.scrollTop = 0;
@@ -379,6 +443,77 @@
// For now, server-side rendering handles highlighting.
}
function enhanceCodeBlocks() {
const blocks = articleBody.querySelectorAll('pre.hljs');
blocks.forEach((block) => {
if (block.parentElement && block.parentElement.classList.contains('code-block-shell')) return;
const shell = document.createElement('div');
shell.className = 'code-block-shell';
block.parentNode.insertBefore(shell, block);
shell.appendChild(block);
const button = document.createElement('button');
button.type = 'button';
button.className = 'code-copy-btn';
button.textContent = '复制';
button.setAttribute('aria-label', '复制代码');
shell.appendChild(button);
});
}
async function writeClipboardText(text) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
let copied = false;
try {
copied = document.execCommand('copy');
} finally {
textarea.remove();
}
if (!copied) {
throw new Error('copy command failed');
}
return true;
}
async function copyCodeFromButton(button) {
const shell = button.closest('.code-block-shell');
const block = shell ? shell.querySelector('pre') : null;
const code = block ? block.querySelector('code') : null;
if (!code) return;
const text = code.textContent || '';
try {
await writeClipboardText(text);
button.textContent = '已复制';
} catch (_) {
button.textContent = '复制失败';
}
window.setTimeout(() => {
if (button.isConnected) {
button.textContent = '复制';
}
}, 1600);
}
function renderArticleMeta(meta) {
const created = formatDate(meta.created);
const modified = formatDate(meta.modified);
@@ -442,11 +577,13 @@
markdownContent.style.display = '';
articleTitle.textContent = initialContent.title;
articleBody.innerHTML = initialContent.html;
enhanceCodeBlocks();
buildTOC();
renderArticleMeta(initialContent.meta);
currentFile = defaultFile;
renderToolbarBreadcrumb(defaultFile, initialContent.title);
if (defaultFile && history.replaceState) {
history.replaceState(null, '', '#' + encodeURIComponent(defaultFile));
}
@@ -472,6 +609,7 @@
}
window.addEventListener('hashchange', handleHashChange);
handleHashChange();
// === Event Listeners ===
btnToggleTheme.addEventListener('click', toggleColorMode);
@@ -490,6 +628,14 @@
scrollToHeading(link.getAttribute('data-target'));
});
}
if (articleBody) {
articleBody.addEventListener('click', function (e) {
const button = e.target.closest('.code-copy-btn');
if (!button) return;
e.preventDefault();
copyCodeFromButton(button);
});
}
// === Keyboard Shortcuts ===
document.addEventListener('keydown', function (e) {