Extract HTML template into separate views file
This commit is contained in:
+125
-45
@@ -3,6 +3,7 @@
|
||||
|
||||
const DATA = window.__WEBOOK__;
|
||||
const navTree = DATA.nav || [];
|
||||
const themes = Array.isArray(DATA.themes) ? DATA.themes : [];
|
||||
const defaultFile = DATA.defaultFile;
|
||||
const initialContent = DATA.initialContent;
|
||||
|
||||
@@ -11,6 +12,7 @@
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const btnToggleSidebar = document.getElementById('btnToggleSidebar');
|
||||
const btnToggleTheme = document.getElementById('btnToggleTheme');
|
||||
const themeSelect = document.getElementById('themeSelect');
|
||||
const contentBody = document.getElementById('contentBody');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const markdownContent = document.getElementById('markdownContent');
|
||||
@@ -22,35 +24,88 @@
|
||||
const loadingBar = document.getElementById('loadingBar');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const mobileNavBtn = document.getElementById('mobileNavBtn');
|
||||
const themeStylesheet = document.getElementById('themeStylesheet');
|
||||
const themeHighlightStylesheet = document.getElementById('themeHighlightStylesheet');
|
||||
|
||||
let currentFile = null;
|
||||
const fallbackTheme = themes[0] || {
|
||||
id: 'default',
|
||||
label: 'Default',
|
||||
defaultMode: 'light',
|
||||
stylesheet: '/_webook/themes/default/theme.css',
|
||||
highlight: {
|
||||
light: '/_webook/themes/default/highlight-light.css',
|
||||
dark: '/_webook/themes/default/highlight-dark.css'
|
||||
}
|
||||
};
|
||||
|
||||
// === Theme ===
|
||||
function getTheme() {
|
||||
return localStorage.getItem('webook-theme') || 'light';
|
||||
function getThemeConfig(themeId) {
|
||||
return themes.find((item) => item.id === themeId) || fallbackTheme;
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('webook-theme', theme);
|
||||
btnToggleTheme.textContent = theme === 'dark' ? '☀︎' : '☾';
|
||||
|
||||
// Swap highlight theme
|
||||
const hlLink = document.querySelector('.theme-highlight');
|
||||
if (hlLink) {
|
||||
hlLink.href = theme === 'dark'
|
||||
? '/_webook/highlight-dark.css'
|
||||
: '/_webook/highlight-light.css';
|
||||
function getStoredThemeId() {
|
||||
const stored = localStorage.getItem('webook-theme-id');
|
||||
if (stored && themes.some((item) => item.id === stored)) {
|
||||
return stored;
|
||||
}
|
||||
return DATA.initialThemeId || fallbackTheme.id;
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = getTheme();
|
||||
setTheme(current === 'light' ? 'dark' : 'light');
|
||||
function getStoredColorMode(themeId) {
|
||||
const stored = localStorage.getItem('webook-color-mode');
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const legacy = localStorage.getItem('webook-theme');
|
||||
if (legacy === 'light' || legacy === 'dark') {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
const theme = getThemeConfig(themeId);
|
||||
return theme.defaultMode === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
setTheme(getTheme());
|
||||
function renderThemeOptions() {
|
||||
if (!themeSelect) return;
|
||||
themeSelect.innerHTML = themes.map((item) => (
|
||||
`<option value="${escapeHtml(item.id)}">${escapeHtml(item.label)}</option>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function applyTheme(themeId, colorMode) {
|
||||
const theme = getThemeConfig(themeId);
|
||||
const mode = colorMode === 'dark' ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.setAttribute('data-theme-id', theme.id);
|
||||
document.documentElement.setAttribute('data-color-mode', mode);
|
||||
localStorage.setItem('webook-theme-id', theme.id);
|
||||
localStorage.setItem('webook-color-mode', mode);
|
||||
localStorage.setItem('webook-theme', mode);
|
||||
|
||||
if (themeStylesheet) {
|
||||
themeStylesheet.href = theme.stylesheet;
|
||||
}
|
||||
if (themeHighlightStylesheet) {
|
||||
themeHighlightStylesheet.href = mode === 'dark' ? theme.highlight.dark : theme.highlight.light;
|
||||
}
|
||||
if (themeSelect) {
|
||||
themeSelect.value = theme.id;
|
||||
}
|
||||
|
||||
btnToggleTheme.textContent = mode === 'dark' ? '☀︎' : '☾';
|
||||
btnToggleTheme.title = mode === 'dark' ? '切换到浅色模式' : '切换到深色模式';
|
||||
}
|
||||
|
||||
function toggleColorMode() {
|
||||
const currentThemeId = getStoredThemeId();
|
||||
const currentMode = getStoredColorMode(currentThemeId);
|
||||
applyTheme(currentThemeId, currentMode === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
renderThemeOptions();
|
||||
applyTheme(getStoredThemeId(), getStoredColorMode(getStoredThemeId()));
|
||||
|
||||
// === Sidebar Toggle ===
|
||||
function getSidebarVisible() {
|
||||
@@ -174,6 +229,10 @@
|
||||
if (fileItem) fileItem.classList.add('active');
|
||||
}
|
||||
|
||||
function findNavFileItem(filePath) {
|
||||
return sidebarNav.querySelector(`[data-file-path="${CSS.escape(filePath)}"]`);
|
||||
}
|
||||
|
||||
// === Search ===
|
||||
searchInput.addEventListener('input', function () {
|
||||
const query = this.value.trim().toLowerCase();
|
||||
@@ -270,16 +329,7 @@
|
||||
articleBody.innerHTML = data.html;
|
||||
buildTOC();
|
||||
|
||||
// Update meta
|
||||
const created = formatDate(data.meta.created);
|
||||
const modified = formatDate(data.meta.modified);
|
||||
const wordCount = data.meta.wordCount;
|
||||
|
||||
articleMeta.innerHTML = `
|
||||
<span class="article-meta-item">📅 创建: ${created}</span>
|
||||
<span class="article-meta-item">✏️ 修改: ${modified}</span>
|
||||
<span class="article-meta-item">📝 字数: ${wordCount.toLocaleString()}</span>
|
||||
`;
|
||||
renderArticleMeta(data.meta);
|
||||
|
||||
// Scroll to top
|
||||
contentBody.scrollTop = 0;
|
||||
@@ -329,6 +379,17 @@
|
||||
// For now, server-side rendering handles highlighting.
|
||||
}
|
||||
|
||||
function renderArticleMeta(meta) {
|
||||
const created = formatDate(meta.created);
|
||||
const modified = formatDate(meta.modified);
|
||||
const wordCount = meta.wordCount;
|
||||
articleMeta.innerHTML = `
|
||||
<span class="article-meta-item">创建时间: ${created}</span>
|
||||
<span class="article-meta-item">修改时间: ${modified}</span>
|
||||
<span class="article-meta-item">字数统计: ${wordCount.toLocaleString()}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function slugifyHeading(text, index) {
|
||||
return 'toc-' + text
|
||||
.toLowerCase()
|
||||
@@ -361,13 +422,20 @@
|
||||
});
|
||||
|
||||
tocNav.innerHTML = items.map((item) => `
|
||||
<a class="toc-link toc-level-${item.level}" href="#${escapeHtml(item.id)}" data-target="${escapeHtml(item.id)}">
|
||||
<button class="toc-link toc-level-${item.level}" type="button" data-target="${escapeHtml(item.id)}">
|
||||
<span class="toc-bullet"></span>
|
||||
<span class="toc-text">${escapeHtml(item.text)}</span>
|
||||
</a>
|
||||
</button>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function scrollToHeading(targetId) {
|
||||
if (!targetId) return;
|
||||
const target = articleBody.querySelector(`#${CSS.escape(targetId)}`);
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// === Initial Load ===
|
||||
if (initialContent) {
|
||||
emptyState.style.display = 'none';
|
||||
@@ -376,14 +444,7 @@
|
||||
articleBody.innerHTML = initialContent.html;
|
||||
buildTOC();
|
||||
|
||||
const created = formatDate(initialContent.meta.created);
|
||||
const modified = formatDate(initialContent.meta.modified);
|
||||
const wordCount = initialContent.meta.wordCount;
|
||||
articleMeta.innerHTML = `
|
||||
<span class="article-meta-item">📅 创建: ${created}</span>
|
||||
<span class="article-meta-item">✏️ 修改: ${modified}</span>
|
||||
<span class="article-meta-item">📝 字数: ${wordCount.toLocaleString()}</span>
|
||||
`;
|
||||
renderArticleMeta(initialContent.meta);
|
||||
|
||||
currentFile = defaultFile;
|
||||
if (defaultFile && history.replaceState) {
|
||||
@@ -394,22 +455,41 @@
|
||||
// === Hash-based navigation ===
|
||||
function handleHashChange() {
|
||||
const hash = decodeURIComponent(window.location.hash.slice(1));
|
||||
if (hash && hash !== currentFile) {
|
||||
if (!hash || hash === currentFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileItem = findNavFileItem(hash);
|
||||
if (fileItem) {
|
||||
loadFile(hash);
|
||||
// Find and activate the nav item
|
||||
const fileItem = sidebarNav.querySelector(`[data-file-path="${CSS.escape(hash)}"]`);
|
||||
if (fileItem) {
|
||||
setActiveFile(fileItem);
|
||||
}
|
||||
setActiveFile(fileItem);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hash.startsWith('toc-')) {
|
||||
scrollToHeading(hash);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
// === Event Listeners ===
|
||||
btnToggleTheme.addEventListener('click', toggleTheme);
|
||||
btnToggleTheme.addEventListener('click', toggleColorMode);
|
||||
btnToggleSidebar.addEventListener('click', toggleSidebar);
|
||||
mobileNavBtn.addEventListener('click', showMobileSidebar);
|
||||
if (themeSelect) {
|
||||
themeSelect.addEventListener('change', function () {
|
||||
applyTheme(this.value, getStoredColorMode(this.value));
|
||||
});
|
||||
}
|
||||
if (tocNav) {
|
||||
tocNav.addEventListener('click', function (e) {
|
||||
const link = e.target.closest('[data-target]');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
scrollToHeading(link.getAttribute('data-target'));
|
||||
});
|
||||
}
|
||||
|
||||
// === Keyboard Shortcuts ===
|
||||
document.addEventListener('keydown', function (e) {
|
||||
|
||||
Reference in New Issue
Block a user