Improve breadcrumb and code block controls
This commit is contained in:
+147
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user