/**
* @namespace TocFeature - Enhanced Modern Version
* @description 管理页面侧边浮动的现代化目录 (Table of Contents) 功能。
* 包括TOC按钮的点击事件、TOC悬浮窗的显示/隐藏、
* 智能层级识别、平滑滚动导航以及动态生成TOC列表项。
*
* 特性:
* - 现代化UI设计
* - 智能标题识别
* - 平滑动画过渡
* - 响应式布局
* - 层级可视化
* - 快速导航
*/
(function EnhancedTocFeature(){
const tocBtn = document.getElementById('toc-float-btn');
const tocPopup = document.getElementById('toc-popup');
const tocList = document.getElementById('toc-list');
const tocCloseBtn = document.getElementById('toc-popup-close-btn');
// 智能TOC缓存系统
let tocCache = {
lastUpdate: 0,
structure: null,
nodes: [],
clearCache: function() {
this.lastUpdate = 0;
this.structure = null;
this.nodes = [];
},
isValid: function() {
return Date.now() - this.lastUpdate < 30000; // 30秒缓存
}
};
// 性能监控
let performanceMetrics = {
renderTime: 0,
nodeCount: 0,
structureComplexity: 0
};
// 智能观察器,监控DOM变化
let contentObserver = null;
// 创建内容变化观察器
function initContentObserver() {
if (!window.MutationObserver) return;
contentObserver = new MutationObserver(function(mutations) {
let shouldRefresh = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
// 检查是否有标题元素的变化
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
(node.matches && node.matches('h1,h2,h3,h4,h5,h6,p') ||
node.querySelector && node.querySelector('h1,h2,h3,h4,h5,h6,p'))) {
shouldRefresh = true;
}
});
}
});
if (shouldRefresh) {
tocCache.clearCache();
if (tocPopup.classList.contains('toc-popup-visible')) {
setTimeout(renderTocList, 500); // 延迟刷新避免频繁更新
}
}
});
const container = document.querySelector('.container');
if (container) {
contentObserver.observe(container, {
childList: true,
subtree: true
});
}
}
// 当前 TOC 显示模式:both, ocr, translation
let currentTocMode = 'both';
// TOC 偏好设置
let tocPreferences = {
autoExpand: localStorage.getItem('toc-auto-expand') !== 'false',
showPreview: localStorage.getItem('toc-show-preview') !== 'false',
compactMode: localStorage.getItem('toc-compact-mode') === 'true',
smartGrouping: localStorage.getItem('toc-smart-grouping') !== 'false'
};
// 添加 TOC 模式切换按钮容器,改为现代化标签页形式
let tocModeSelector = document.createElement('div');
tocModeSelector.className = 'toc-mode-selector';
tocModeSelector.innerHTML = `
`;
// 将模式选择器插入到 TOC 弹窗头部下方
if (tocPopup) {
const tocHeader = tocPopup.querySelector('#toc-popup-header');
if (tocHeader) {
tocHeader.parentNode.insertBefore(tocModeSelector, tocHeader.nextSibling);
}
}
// 绑定模式切换按钮事件
tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(btn => {
btn.addEventListener('click', function() {
const mode = this.dataset.mode;
if (currentTocMode === mode) return; // 避免重复切换
currentTocMode = mode;
tocCache.clearCache(); // 清除缓存
// 更新按钮状态
tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(b => {
b.classList.remove('active');
});
this.classList.add('active');
// 添加切换动画
const tocListElement = document.getElementById('toc-list');
tocListElement.style.opacity = '0.5';
tocListElement.style.transform = 'translateY(10px)';
// 重新渲染 TOC 列表
setTimeout(() => {
renderTocList();
tocListElement.style.opacity = '';
tocListElement.style.transform = '';
}, 150);
});
});
// 添加现代化底部控制区域
const tocControls = document.createElement('div');
tocControls.className = 'toc-controls';
tocControls.innerHTML = `
`;
// 将控制区域添加到TOC弹窗
if (tocPopup) {
tocPopup.appendChild(tocControls);
}
/**
* TOC映射表 - 支持更多语言对照
*/
const tocMap = {
'历史详情': 'History Detail',
'OCR内容': 'OCR Content',
'仅OCR': 'OCR Only',
'翻译内容': 'Translation',
'仅翻译': 'Translation Only',
'分块对比': 'Chunk Compare',
'摘要': 'Abstract',
'引言': 'Introduction',
'方法': 'Methods',
'结果': 'Results',
'讨论': 'Discussion',
'结论': 'Conclusion',
'参考文献': 'References',
'附录': 'Appendix'
};
/**
* 存储TOC列表项对应的页面内标题DOM元素
*/
let tocNodes = [];
/**
* 现代化智能文本截断函数
* @param {string} text - 要截断的文本
* @param {number} maxLength - 最大长度
* @returns {string} 截断后的文本
*/
function smartTruncateText(text, maxLength = 35) {
if (!text || text.length <= maxLength) return text;
// 检查是否是图表标题
const isChartTitle = /^(图|表|Figure|Table)\s*\d+/i.test(text);
// 对于图表标题,使用更智能的截断策略
if (isChartTitle) {
const titleMatch = text.match(/^(图|表|Figure|Table)\s*\d+[\.:\:]?\s*(.*)$/i);
if (titleMatch) {
const prefix = titleMatch[1];
const content = titleMatch[2] || '';
// 在内容中查找合适的截断点
const sentenceEnd = content.search(/[。!?\.!?]/);
if (sentenceEnd > 0 && sentenceEnd <= maxLength - prefix.length - 5) {
return prefix + content.substring(0, sentenceEnd + 1);
}
}
}
// 智能截断:优先在标点符号处截断
const punctuationRegex = /[。,!?;:、\.,!?;:]/g;
let match;
let lastPunctIndex = -1;
while ((match = punctuationRegex.exec(text)) !== null) {
if (match.index < maxLength - 3) {
lastPunctIndex = match.index;
} else {
break;
}
}
if (lastPunctIndex > maxLength * 0.6) {
return text.substring(0, lastPunctIndex + 1);
}
// 在空格处截断
const spaceIndex = text.lastIndexOf(' ', maxLength - 3);
if (spaceIndex > maxLength * 0.7) {
return text.substring(0, spaceIndex) + '...';
}
// 最后使用硬截断
return text.substring(0, maxLength - 3) + '...';
}
/**
* 现代化临时加载效果,带进度指示
* @param {string} sectionName - 正在导航到的章节名称
*/
function showEnhancedLoadingEffect(sectionName) {
let effectDiv = document.getElementById('toc-loading-effect');
const mainContainer = document.querySelector('.container');
if (!effectDiv) {
effectDiv = document.createElement('div');
effectDiv.id = 'toc-loading-effect';
effectDiv.className = 'loading-effect';
document.body.appendChild(effectDiv);
}
if (mainContainer) {
mainContainer.classList.add('content-blurred');
}
const truncatedSectionName = smartTruncateText(sectionName, 30);
effectDiv.innerHTML = `
正在前往
${truncatedSectionName}
`;
requestAnimationFrame(() => {
effectDiv.classList.add('loading-effect-visible');
// 启动进度条动画
const progressBar = effectDiv.querySelector('.progress-bar');
if (progressBar) {
progressBar.style.width = '0%';
setTimeout(() => {
progressBar.style.width = '100%';
}, 100);
}
});
setTimeout(() => {
effectDiv.classList.remove('loading-effect-visible');
if (mainContainer) {
mainContainer.classList.remove('content-blurred');
}
}, 1800);
}
/**
* 现代化平滑切换TOC项的折叠状态
* @param {HTMLElement} toggleBtn - 折叠/展开按钮元素
* @param {HTMLElement} childrenContainer - 子项容器元素
*/
function toggleTocItem(toggleBtn, childrenContainer) {
const isCollapsed = toggleBtn.classList.contains('collapsed');
const listItem = toggleBtn.closest('li');
if (isCollapsed) {
// 展开动画
toggleBtn.classList.remove('collapsed');
childrenContainer.classList.remove('collapsed');
// 计算目标高度
childrenContainer.style.height = '0';
childrenContainer.style.opacity = '0';
childrenContainer.style.transform = 'translateY(-10px)';
const targetHeight = Array.from(childrenContainer.children)
.reduce((height, child) => height + child.offsetHeight, 0);
// 触发动画
requestAnimationFrame(() => {
childrenContainer.style.height = targetHeight + 'px';
childrenContainer.style.opacity = '1';
childrenContainer.style.transform = 'translateY(0)';
});
// 动画完成后清理样式
setTimeout(() => {
childrenContainer.style.height = 'auto';
}, 300);
// 添加展开状态指示
if (listItem) {
listItem.classList.add('toc-expanded');
}
} else {
// 折叠动画
const currentHeight = childrenContainer.offsetHeight;
childrenContainer.style.height = currentHeight + 'px';
requestAnimationFrame(() => {
toggleBtn.classList.add('collapsed');
childrenContainer.style.height = '0';
childrenContainer.style.opacity = '0';
childrenContainer.style.transform = 'translateY(-10px)';
});
setTimeout(() => {
childrenContainer.classList.add('collapsed');
}, 300);
// 移除展开状态指示
if (listItem) {
listItem.classList.remove('toc-expanded');
}
}
// 保存用户的折叠偏好
const itemId = listItem?.querySelector('a')?.getAttribute('href');
if (itemId) {
const collapsedItems = JSON.parse(localStorage.getItem('toc-collapsed-items') || '[]');
if (isCollapsed) {
// 展开:从折叠列表中移除
const index = collapsedItems.indexOf(itemId);
if (index > -1) collapsedItems.splice(index, 1);
} else {
// 折叠:添加到折叠列表
if (!collapsedItems.includes(itemId)) {
collapsedItems.push(itemId);
}
}
localStorage.setItem('toc-collapsed-items', JSON.stringify(collapsedItems));
}
}
// 现代化打开/关闭悬浮窗
tocBtn.onclick = function() {
const isOpen = tocPopup.classList.contains('toc-popup-visible');
if (isOpen) {
// 关闭动画
tocPopup.classList.remove('toc-popup-visible');
tocPopup.classList.add('toc-popup-hiding');
setTimeout(() => {
tocPopup.classList.remove('toc-popup-hiding');
tocPopup.classList.add('toc-popup-hidden');
}, 400);
} else {
// 打开前检查和更新内容
updateTocModeSelectorVisibility();
// 如果缓存无效,重新渲染
if (!tocCache.isValid()) {
renderTocList();
}
// 打开动画
tocPopup.classList.remove('toc-popup-hidden', 'toc-popup-hiding');
tocPopup.classList.add('toc-popup-visible');
// 延迟聚焦以改善用户体验
setTimeout(() => {
const firstLink = tocPopup.querySelector('#toc-list a');
if (firstLink) {
firstLink.focus();
}
}, 100);
}
};
// 关闭悬浮窗按钮
tocCloseBtn.onclick = function() {
tocPopup.classList.remove('toc-popup-visible');
tocPopup.classList.add('toc-popup-hiding');
setTimeout(() => {
tocPopup.classList.remove('toc-popup-hiding');
tocPopup.classList.add('toc-popup-hidden');
}, 400);
};
/**
* 更新TOC模式选择器的可见性,仅在分块对比模式下显示
*/
function updateTocModeSelectorVisibility() {
const visibleTab = document.querySelector('.tab-btn.active');
const currentTabId = visibleTab ? visibleTab.id : null;
const isChunkCompareMode = currentTabId === 'tab-chunk-compare';
if (isChunkCompareMode) {
tocModeSelector.style.display = 'flex';
} else {
tocModeSelector.style.display = 'none';
if (currentTocMode !== 'both') {
currentTocMode = 'both';
tocModeSelector.querySelectorAll('.toc-mode-btn').forEach(b => {
b.classList.remove('active');
});
tocModeSelector.querySelector('[data-mode="both"]').classList.add('active');
}
}
}
/**
* 增强的智能层级管理器
*/
let enhancedLevelManager = {
prefixMapping: {},
contextStack: [],
lastStructureInfo: null,
analyzeHeading: function(text) {
const patterns = {
chapter: /^第[一二三四五六七八九十百千万]+[章节篇部]/,
numeric: /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?/,
roman: /^([IVX]+)(?:\.([IVX]+))?(?:\.([IVX]+))?/i,
bulletList: /^[•\*\-]\s+/,
numberedList: /^(\d+)(?:[\.、]|\s*[\(\(])\s*/,
specialSection: /^(摘要|Abstract|引言|Introduction|参考文献|References|附录|Appendix|致谢|Acknowledgements|结论|Conclusion|讨论|Discussion|实验|Experiment|方法|Methods|材料|Materials)/i
};
let structureInfo = {
type: 'normal',
level: null,
prefix: '',
content: text,
confidence: 0
};
// 检测各种模式并计算置信度
for (const [type, pattern] of Object.entries(patterns)) {
const match = text.match(pattern);
if (match) {
structureInfo.type = type;
structureInfo.prefix = match[0];
structureInfo.content = text.substring(match[0].length).trim();
structureInfo.confidence = this.calculateConfidence(type, match);
// 根据类型确定层级
structureInfo.level = this.determineLevelByType(type, match);
break;
}
}
// 更新上下文栈
this.updateContextStack(structureInfo);
this.lastStructureInfo = structureInfo;
return structureInfo;
},
calculateConfidence: function(type, match) {
// 基于模式复杂度和匹配质量计算置信度
const confidenceMap = {
'specialSection': 0.95,
'chapter': 0.9,
'numeric': 0.85,
'roman': 0.8,
'numberedList': 0.7,
'bulletList': 0.6
};
return confidenceMap[type] || 0.5;
},
determineLevelByType: function(type, match) {
switch (type) {
case 'specialSection':
case 'chapter':
return 1;
case 'numeric':
return (match[0].match(/\./g) || []).length + 1;
case 'roman':
return (match[0].match(/\./g) || []).length + 1;
case 'numberedList':
case 'bulletList':
return (this.lastStructureInfo?.level || 1) + 1;
default:
return 2;
}
},
updateContextStack: function(structureInfo) {
// 维护结构化上下文栈
if (structureInfo.level) {
// 移除更深层级的项目
this.contextStack = this.contextStack.filter(item => item.level < structureInfo.level);
this.contextStack.push(structureInfo);
}
}
};
/**
* 主要的TOC渲染函数 - 增强版
*/
function renderTocList() {
const startTime = performance.now();
// 检查缓存
if (tocCache.isValid() && tocCache.structure) {
buildTocHtml(tocCache.structure.children, tocList);
return;
}
tocList.innerHTML = '';
tocNodes = [];
const container = document.querySelector('.container');
if (!container) return;
// 收集所有潜在标题
let potentialHeadings = [];
container.querySelectorAll('h1, h2:not(#fileName), h3, h4, h5, h6, p.converted-from-heading').forEach(h => {
potentialHeadings.push(h);
});
// 添加图表标题
const captionRegex = /^(图|表|Figure|Table)\s*[\d\w.-]+\b/i;
container.querySelectorAll('p').forEach(p => {
const text = p.textContent.trim();
if (captionRegex.test(text)) {
p.dataset.isCaptionToc = "true";
p.dataset.isChartCaption = "true";
potentialHeadings.push(p);
}
});
// 按文档顺序排序
potentialHeadings.sort((a, b) => {
const position = a.compareDocumentPosition(b);
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
// 根据当前模式过滤
let filteredHeadings = filterHeadingsByMode(potentialHeadings);
// 构建TOC结构
const tocStructure = buildTocStructure(filteredHeadings);
// 缓存结果
tocCache.structure = tocStructure;
tocCache.nodes = tocNodes;
tocCache.lastUpdate = Date.now();
// 渲染HTML
buildTocHtml(tocStructure.children, tocList);
// 恢复折叠状态
restoreCollapsedState();
// 记录性能指标
performanceMetrics.renderTime = performance.now() - startTime;
performanceMetrics.nodeCount = tocNodes.length;
console.log(`TOC渲染完成: ${performanceMetrics.nodeCount}个节点, 耗时${performanceMetrics.renderTime.toFixed(2)}ms`);
}
/**
* 根据模式过滤标题
*/
function filterHeadingsByMode(headings) {
if (currentTocMode === 'both') {
return headings;
}
const visibleTab = document.querySelector('.tab-btn.active');
const currentTabId = visibleTab ? visibleTab.id : null;
const isChunkCompareMode = currentTabId === 'tab-chunk-compare';
if (isChunkCompareMode) {
const selector = currentTocMode === 'ocr' ? '.align-block-ocr' : '.align-block-trans';
return headings.filter(el => el.closest(selector) !== null);
} else {
const expectedTabId = currentTocMode === 'ocr' ? 'tab-ocr' : 'tab-translation';
if (currentTabId === expectedTabId) {
return headings;
} else {
// 显示提示信息
const li = document.createElement('li');
li.className = 'toc-info';
li.innerHTML = `
请切换到${currentTocMode === 'ocr' ? '原文' : '译文'}标签页查看对应目录
`;
tocList.appendChild(li);
return [];
}
}
}
/**
* 构建TOC层级结构
*/
function buildTocStructure(headings) {
const structure = { root: true, children: [] };
let currentPath = [structure];
let previousLevel = 0;
headings.forEach((nodeEl, idx) => {
if (!nodeEl.id) nodeEl.id = 'toc-auto-' + idx;
tocNodes.push(nodeEl);
const text = nodeEl.textContent.trim();
if (text.includes('原文块') || text.includes('译文块')) return;
// 使用增强的层级管理器分析
const structureInfo = enhancedLevelManager.analyzeHeading(text);
const level = structureInfo.level || getDefaultLevel(nodeEl);
// 调整路径
adjustPath(currentPath, level, previousLevel);
// 创建TOC项
const tocItem = createTocItem(nodeEl, text, level, structureInfo);
currentPath[currentPath.length - 1].children.push(tocItem);
previousLevel = level;
});
return structure;
}
/**
* 获取默认层级
*/
function getDefaultLevel(element) {
const tagName = element.tagName.toLowerCase();
if (tagName.match(/^h[1-6]$/)) {
return parseInt(tagName.substring(1));
}
return element.dataset.isChartCaption === "true" ? 4 : 3;
}
/**
* 调整当前路径
*/
function adjustPath(path, currentLevel, previousLevel) {
if (currentLevel > previousLevel) {
// 进入更深层级
while (path.length < currentLevel) {
if (path[path.length - 1].children.length === 0) {
// 创建占位符
const placeholder = {
id: 'placeholder-' + Date.now(),
text: '未命名章节',
level: path.length,
children: []
};
path[path.length - 1].children.push(placeholder);
}
path.push(path[path.length - 1].children[path[path.length - 1].children.length - 1]);
}
} else if (currentLevel < previousLevel) {
// 返回上层
const levelsToGoUp = previousLevel - currentLevel;
for (let i = 0; i < levelsToGoUp && path.length > 1; i++) {
path.pop();
}
}
}
/**
* 创建TOC项
*/
function createTocItem(element, text, level, structureInfo) {
const displayText = smartTruncateText(text);
const translation = tocMap[text];
return {
id: element.id,
text: displayText,
originalText: text,
translation: translation,
level: level,
children: [],
isChartCaption: element.dataset.isChartCaption === "true",
structureInfo: structureInfo,
element: element
};
}
/**
* 构建TOC HTML - 增强版
*/
function buildTocHtml(items, parentElement) {
items.forEach(item => {
if (item.id?.indexOf('placeholder') === 0 && !item.text) return;
const li = document.createElement('li');
const hasChildren = item.children && item.children.length > 0;
// 设置CSS类
li.className = getTocItemClasses(item, hasChildren);
// 构建链接HTML
const linkHTML = buildLinkHTML(item, hasChildren);
const link = document.createElement('a');
link.href = `#${item.id}`;
link.innerHTML = linkHTML;
link.dataset.originalText = item.originalText;
// 添加现代化点击事件
addEnhancedClickHandler(link, item);
li.appendChild(link);
// 添加子项
if (hasChildren) {
const childrenContainer = document.createElement('ul');
childrenContainer.className = 'toc-children';
buildTocHtml(item.children, childrenContainer);
li.appendChild(childrenContainer);
// 添加折叠按钮事件
addToggleHandler(li);
}
parentElement.appendChild(li);
});
}
/**
* 获取TOC项的CSS类
*/
function getTocItemClasses(item, hasChildren) {
let classes = [];
if (item.level) {
if (item.isChartCaption) {
classes.push('toc-caption');
} else {
classes.push(`toc-h${item.level}`);
}
}
if (hasChildren) {
classes.push('has-children');
}
if (item.structureInfo?.type && item.structureInfo.type !== 'normal') {
classes.push('toc-structured', `toc-structure-${item.structureInfo.type}`);
}
return classes.join(' ');
}
/**
* 构建链接HTML
*/
function buildLinkHTML(item, hasChildren) {
let html = '';
if (hasChildren) {
html += '▼';
}
html += '';
// 添加结构化前缀
if (item.structureInfo?.prefix) {
html += `${item.structureInfo.prefix}`;
}
// 添加图表图标
if (item.isChartCaption) {
const isTable = item.originalText?.startsWith('表');
const icon = isTable ? '📊' : '📈';
html += `${icon}`;
}
// 添加内容
let displayText = item.text;
if (item.structureInfo?.prefix && displayText.startsWith(item.structureInfo.prefix)) {
displayText = displayText.substring(item.structureInfo.prefix.length).trim();
}
html += `${displayText}`;
// 添加翻译
if (item.translation && item.translation !== item.originalText) {
html += `/ ${item.translation}`;
}
html += '';
return html;
}
/**
* 添加增强的点击处理器
*/
function addEnhancedClickHandler(link, item) {
link.onclick = function(e) {
e.preventDefault();
console.log('[TOC Debug] TOC 点击事件触发:', item.id);
const targetElement = document.getElementById(item.id);
if (!targetElement) {
console.log('[TOC Debug] 未找到目标元素:', item.id);
return;
}
// 计算距离并决定是否显示加载效果
const clickedNodeIndex = tocNodes.findIndex(n => n.id === item.id);
const currentTopNodeIndex = getCurrentTopNodeIndex();
const indexDifference = Math.abs(clickedNodeIndex - currentTopNodeIndex);
if (indexDifference >= 6) {
showEnhancedLoadingEffect(item.originalText || "目标章节");
}
console.log('[TOC Debug] 检查沉浸模式:', {
hasImmersiveLayout: !!window.ImmersiveLayout,
isActive: window.ImmersiveLayout?.isActive()
});
// 修复:在沉浸模式下使用自定义滚动逻辑,避免布局偏移
if (window.ImmersiveLayout && window.ImmersiveLayout.isActive()) {
console.log('[TOC Debug] 进入沉浸模式分支');
// 沉浸模式下使用自定义滚动定位
// 优先查找 .content-wrapper(真正的滚动容器)
let scrollContainer = document.querySelector('#immersive-main-content-area .content-wrapper');
// 后备方案 1:查找 .js-scroll-container 标记
if (!scrollContainer) {
scrollContainer = document.querySelector('#immersive-main-content-area .js-scroll-container');
}
// 后备方案 2:查找 .tab-content
if (!scrollContainer) {
scrollContainer = document.querySelector('#immersive-main-content-area .tab-content');
}
if (scrollContainer) {
// 使用 computed style 检查是否可滚动(而不是检查内联样式)
const computedStyle = getComputedStyle(scrollContainer);
const overflowY = computedStyle.overflowY;
const isScrollable = (overflowY === 'auto' || overflowY === 'scroll');
console.log('[TOC Debug] 沉浸模式滚动检测:', {
scrollContainer: scrollContainer.className,
overflowY,
isScrollable,
scrollHeight: scrollContainer.scrollHeight,
clientHeight: scrollContainer.clientHeight
});
// 只要找到了滚动容器,就尝试滚动(即使当前没有滚动条)
if (isScrollable) {
// 计算目标元素在滚动容器内的绝对位置
const containerRect = scrollContainer.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();
const currentScrollTop = scrollContainer.scrollTop;
// 目标元素相对于容器内容的绝对位置 = 当前滚动位置 + 目标相对于容器视口的位置
const targetOffsetInContainer = currentScrollTop + (targetRect.top - containerRect.top);
// 改进的滚动逻辑:确保目标元素可见,但不滚动过头
// 如果目标元素已经在视口内,就不滚动
const viewportTop = containerRect.top;
const viewportBottom = containerRect.bottom;
const targetTop = targetRect.top;
const targetBottom = targetRect.bottom;
// 目标元素已经完全可见,不需要滚动
if (targetTop >= viewportTop && targetBottom <= viewportBottom) {
return;
}
// 目标元素在视口上方,需要向上滚动
if (targetTop < viewportTop) {
const scrollDelta = targetTop - viewportTop;
scrollContainer.scrollTo({
top: currentScrollTop + scrollDelta,
behavior: 'smooth'
});
return;
}
// 目标元素在视口下方,需要向下滚动
// 将目标元素滚动到视口底部附近
if (targetBottom > viewportBottom) {
const scrollDelta = targetBottom - viewportBottom + 20; // 底部留 20px 空隙
scrollContainer.scrollTo({
top: currentScrollTop + scrollDelta,
behavior: 'smooth'
});
return;
}
}
}
// 如果没有找到滚动容器,不调用 scrollIntoView,避免滚动 overflow:hidden 的祖先容器
return;
} else {
// 普通模式下,检查是否需要滚动 .tab-content 容器(OCR/翻译模式)
const tabContent = document.querySelector('.tab-content');
// 检查 tabContent 是否是滚动容器
if (tabContent) {
const computedStyle = getComputedStyle(tabContent);
const overflowY = computedStyle.overflowY;
const overflow = computedStyle.overflow;
// 支持 auto 和 scroll
const isScrollable = (overflowY === 'auto' || overflowY === 'scroll' || overflow === 'auto' || overflow === 'scroll');
const hasScroll = tabContent.scrollHeight > tabContent.clientHeight;
if (isScrollable && hasScroll) {
// 计算目标元素相对于滚动容器的位置
const containerRect = tabContent.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();
const currentScrollTop = tabContent.scrollTop;
// 计算目标位置(将元素置于容器中心)
const targetScrollTop = currentScrollTop + targetRect.top - containerRect.top - (containerRect.height / 2) + (targetRect.height / 2);
// 平滑滚动到目标位置
tabContent.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
});
} else {
// 如果 tab-content 不是滚动容器或不需要滚动,使用原生 scrollIntoView
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
} else {
// tab-content 不存在,使用原生 scrollIntoView
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
}
// 添加高亮效果
addHighlightEffect(targetElement);
};
}
/**
* 获取当前顶部节点索引
*/
function getCurrentTopNodeIndex() {
if (tocNodes.length === 0) return 0;
let minPositiveTop = Infinity;
let topIndex = 0;
for (let i = 0; i < tocNodes.length; i++) {
const rect = tocNodes[i].getBoundingClientRect();
if (rect.top >= 0 && rect.top < minPositiveTop) {
minPositiveTop = rect.top;
topIndex = i;
}
}
return topIndex;
}
/**
* 添加高亮效果
*/
function addHighlightEffect(element) {
element.classList.add('toc-target-highlight');
setTimeout(() => {
element.classList.remove('toc-target-highlight');
}, 3000);
}
/**
* 添加折叠按钮处理器
*/
function addToggleHandler(li) {
const toggleBtn = li.querySelector('.toc-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
const childContainer = this.closest('li').querySelector('.toc-children');
if (childContainer) {
toggleTocItem(this, childContainer);
}
});
}
}
/**
* 恢复折叠状态
*/
function restoreCollapsedState() {
const collapsedItems = JSON.parse(localStorage.getItem('toc-collapsed-items') || '[]');
collapsedItems.forEach(itemId => {
const link = tocList.querySelector(`a[href="${itemId}"]`);
if (link) {
const li = link.closest('li');
const toggleBtn = li.querySelector('.toc-toggle');
const childrenContainer = li.querySelector('.toc-children');
if (toggleBtn && childrenContainer) {
toggleBtn.classList.add('collapsed');
childrenContainer.classList.add('collapsed');
}
}
});
}
// 键盘导航支持
function addKeyboardNavigation() {
tocPopup.addEventListener('keydown', function(e) {
const focusedElement = document.activeElement;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
navigateToNext(focusedElement);
break;
case 'ArrowUp':
e.preventDefault();
navigateToPrevious(focusedElement);
break;
case 'Enter':
case ' ':
if (focusedElement.classList.contains('toc-toggle')) {
e.preventDefault();
focusedElement.click();
}
break;
case 'Escape':
e.preventDefault();
tocCloseBtn.click();
break;
}
});
}
function navigateToNext(current) {
const allFocusable = tocPopup.querySelectorAll('a, .toc-toggle, .toc-control-btn');
const currentIndex = Array.from(allFocusable).indexOf(current);
const nextElement = allFocusable[currentIndex + 1];
if (nextElement) nextElement.focus();
}
function navigateToPrevious(current) {
const allFocusable = tocPopup.querySelectorAll('a, .toc-toggle, .toc-control-btn');
const currentIndex = Array.from(allFocusable).indexOf(current);
const prevElement = allFocusable[currentIndex - 1];
if (prevElement) prevElement.focus();
}
// 控制按钮事件绑定
function bindControlEvents() {
// 展开/收起目录
document.getElementById('toc-expand-btn').addEventListener('click', function() {
const isExpanded = tocPopup.classList.contains('toc-expanded');
const icon = this.querySelector('i');
const text = this.querySelector('span');
if (isExpanded) {
tocPopup.classList.remove('toc-expanded');
icon.className = 'fas fa-expand-arrows-alt';
text.textContent = '展开';
this.title = '展开目录';
} else {
tocPopup.classList.add('toc-expanded');
icon.className = 'fas fa-compress-arrows-alt';
text.textContent = '收起';
this.title = '收起目录';
}
});
// 全部展开
document.getElementById('toc-expand-all').addEventListener('click', function() {
const allToggleButtons = tocList.querySelectorAll('.toc-toggle.collapsed');
allToggleButtons.forEach(btn => {
const childrenContainer = btn.closest('li').querySelector('.toc-children');
if (childrenContainer) {
toggleTocItem(btn, childrenContainer);
}
});
});
// 全部折叠
document.getElementById('toc-collapse-all').addEventListener('click', function() {
const allToggleButtons = tocList.querySelectorAll('.toc-toggle:not(.collapsed)');
allToggleButtons.forEach(btn => {
const childrenContainer = btn.closest('li').querySelector('.toc-children');
if (childrenContainer) {
toggleTocItem(btn, childrenContainer);
}
});
});
}
// 全局刷新函数
window.refreshTocList = function() {
tocCache.clearCache();
updateTocModeSelectorVisibility();
renderTocList();
};
// 初始化
function init() {
updateTocModeSelectorVisibility();
renderTocList();
addKeyboardNavigation();
bindControlEvents();
initContentObserver();
// 监听标签页切换
document.querySelectorAll('.tab-btn').forEach(tab => {
tab.addEventListener('click', () => {
setTimeout(updateTocModeSelectorVisibility, 100);
});
});
console.log('Enhanced TOC initialized successfully');
}
// 启动初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 暴露API
window.EnhancedTocFeature = {
refresh: window.refreshTocList,
getNodes: () => tocNodes,
getStructure: () => tocCache.structure,
getMetrics: () => performanceMetrics,
setMode: (mode) => {
if (['both', 'ocr', 'translation'].includes(mode)) {
currentTocMode = mode;
renderTocList();
}
}
};
})();