// js/annotation_logic.js // 假设以下全局变量由 history_detail.html 或 window 提供: // - window.data (包含批注信息) // - window.globalCurrentContentIdentifier (字符串, 'ocr' 或 'translation') // - window.globalCurrentSelection (对象 {text, range, annotationId?, blockIndex?, subBlockId?, targetElement?, contentIdentifierForSelection?}) // - window.globalCurrentTargetElement (DOM 元素) - 将被 globalCurrentSelection.targetElement 取代或辅助 // - window.globalCurrentHighlightStatus (布尔值) // - getQueryParam (来自 history_detail.html 的函数) // 以及来自 storage.js 的函数: // - saveAnnotationToDB, deleteAnnotationFromDB, updateAnnotationInDB, getAnnotationsForDocFromDB var annotationContextMenuElement; // 右键菜单的HTML元素 // ========== Phase 2.3: 批注系统 DOM 缓存优化 ========== /** * 批注系统 DOM 缓存类 * 缓存 sub-block 元素,避免右键时全文档 querySelectorAll */ const AnnotationDOMCache = { // 缓存的 sub-block 元素数组 subBlocks: null, // 缓存的 sub-block 映射 (subBlockId -> element) subBlockMap: null, // 缓存是否已初始化 initialized: false, /** * 初始化缓存 * 在内容渲染完成后调用 */ init: function() { console.time('[AnnotationCache] 初始化 sub-block 缓存'); // 查询所有 sub-block 元素 this.subBlocks = Array.from(document.querySelectorAll('.sub-block[data-sub-block-id]')); // 创建映射表 this.subBlockMap = new Map(); this.subBlocks.forEach(subBlock => { const subBlockId = subBlock.dataset.subBlockId; if (subBlockId) { this.subBlockMap.set(subBlockId, subBlock); } }); this.initialized = true; console.timeEnd('[AnnotationCache] 初始化 sub-block 缓存'); console.log(`[AnnotationCache] 已缓存 ${this.subBlocks.length} 个 sub-block 元素`); return this; }, /** * 获取所有 sub-block 元素(从缓存) * 如果缓存未初始化,则动态查询 */ getAllSubBlocks: function() { if (!this.initialized) { console.warn('[AnnotationCache] 缓存未初始化,执行动态查询'); return document.querySelectorAll('.sub-block[data-sub-block-id]'); } return this.subBlocks; }, /** * 根据 subBlockId 获取元素 */ getSubBlockById: function(subBlockId) { if (!this.initialized) { console.warn('[AnnotationCache] 缓存未初始化,执行动态查询'); return document.querySelector(`.sub-block[data-sub-block-id="${subBlockId}"]`); } return this.subBlockMap.get(subBlockId) || null; }, /** * 清空缓存 * 在标签切换或内容重新渲染时调用 */ clear: function() { this.subBlocks = null; this.subBlockMap = null; this.initialized = false; console.log('[AnnotationCache] 缓存已清空'); }, /** * 重新初始化缓存 * 在内容更新(如自动分块)后调用 */ refresh: function() { console.log('[AnnotationCache] 刷新缓存...'); this.clear(); return this.init(); } }; // 挂载到全局,方便外部调用 window.AnnotationDOMCache = AnnotationDOMCache; // 这些全局变量将在 history_detail.html 的主脚本中初始化和管理。 // 此脚本将使用它们。 // let globalCurrentSelection = null; // 全局当前选区对象 // let globalCurrentTargetElement = null; // 全局当前右键菜单目标元素 // let globalCurrentHighlightStatus = false; // 全局当前高亮状态 // let globalCurrentContentIdentifier = ''; // 全局当前内容标识符 (例如 'ocr', 'translation'),将由 history_detail.html 中的 showTab 函数设置 function _page_generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function escapeRegExp(string) { // 更安全地转义所有正则表达式特殊字符 return string.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&'); } function fuzzyRegFromExact(exact) { // 先转义所有正则表达式特殊字符 let pattern = escapeRegExp(exact); // 将所有空白替换为 \\s+,允许跨行、多个空格 pattern = pattern.replace(/\\\\s+/g, '\\\\s+'); // 可选:忽略前后空白 pattern = '\\\\s*' + pattern + '\\\\s*'; return new RegExp(pattern, 'gi'); } /** * 模糊匹配两个字符串,忽略所有空白和换行 * @param {string} a 字符串a * @param {string} b 字符串b * @returns {boolean} 如果匹配则返回true,否则返回false */ function fuzzyMatch(a, b) { const cleanA = String(a).replace(/\\s+/g, ''); const cleanB = String(b).replace(/\\s+/g, ''); return cleanA === cleanB; } /** * 通用函数:检查指定目标是否已被高亮。 * @param {string} [annotationId=null] - 可选的批注ID。 * @param {string} contentIdentifier - 当前内容的标识符 ('ocr' 或 'translation')。 * @param {string} [targetIdentifier=null] - 目标元素的标识符 (blockIndex 或 subBlockId)。 * @param {'blockIndex'|'subBlockId'} identifierType - 标识符的类型。 * @returns {boolean} 是否已高亮。 */ function checkIfTargetIsHighlighted(annotationId = null, contentIdentifier, targetIdentifier = null, identifierType) { // console.log(`[checkIfTargetIsHighlighted] ID: ${annotationId}, ContentID: ${contentIdentifier}, TargetID: ${targetIdentifier}, Type: ${identifierType}`); if (!window.data || !window.data.annotations) { return false; } let annotation; if (annotationId) { // ID 优先匹配 annotation = window.data.annotations.find(ann => ann.targetType === contentIdentifier && ann.id === annotationId ); } else if (targetIdentifier !== null && identifierType) { // 通过目标标识符查找,与removeAnnotationFromTarget保持一致的匹配逻辑 const targetIdStr = String(targetIdentifier).trim(); annotation = window.data.annotations.find(ann => { // 确保基本条件匹配 if (ann.targetType !== contentIdentifier) return false; if (!ann.target || !Array.isArray(ann.target.selector) || !ann.target.selector[0]) return false; // 获取选择器中的标识符,确保转换为字符串进行比较 const selectorId = ann.target.selector[0][identifierType]; if (selectorId === undefined) return false; const selectorIdStr = String(selectorId).trim(); // 使用与removeAnnotationFromTarget相同的比较逻辑 return selectorIdStr === targetIdStr || Math.abs(Number(selectorIdStr) - Number(targetIdStr)) < 0.001; }); } // console.log('[checkIfTargetIsHighlighted] 找到的批注:', annotation, '结果:', !!annotation); return !!annotation; } /** * 通用函数:检查指定目标是否已有批注内容。 * @param {string} [annotationId=null] - 可选的批注ID。 * @param {string} contentIdentifier - 当前内容的标识符 ('ocr' 或 'translation')。 * @param {string} [targetIdentifier=null] - 目标元素的标识符 (blockIndex 或 subBlockId)。 * @param {'blockIndex'|'subBlockId'} identifierType - 标识符的类型。 * @returns {boolean} 是否已有批注内容。 */ function checkIfTargetHasNote(annotationId = null, contentIdentifier, targetIdentifier = null, identifierType) { // console.log(`[checkIfTargetHasNote] ID: ${annotationId}, ContentID: ${contentIdentifier}, TargetID: ${targetIdentifier}, Type: ${identifierType}`); if (!window.data || !window.data.annotations) return false; let annotation; if (annotationId) { // ID 优先匹配 annotation = window.data.annotations.find(ann => ann.targetType === contentIdentifier && ann.id === annotationId && ann.body && ann.body.length > 0 && ann.body[0].value && ann.body[0].value.trim() !== '' ); } else if (targetIdentifier !== null && identifierType) { // 通过目标标识符查找,与其他函数保持一致的匹配逻辑 const targetIdStr = String(targetIdentifier).trim(); annotation = window.data.annotations.find(ann => { // 确保基本条件匹配 if (ann.targetType !== contentIdentifier) return false; if (!ann.target || !Array.isArray(ann.target.selector) || !ann.target.selector[0]) return false; // 获取选择器中的标识符,确保转换为字符串进行比较 const selectorId = ann.target.selector[0][identifierType]; if (selectorId === undefined) return false; const selectorIdStr = String(selectorId).trim(); // 使用与其他函数相同的比较逻辑 const idMatch = selectorIdStr === targetIdStr || Math.abs(Number(selectorIdStr) - Number(targetIdStr)) < 0.001; // 还需要检查是否有批注内容 return idMatch && ann.body && ann.body.length > 0 && ann.body[0].value && ann.body[0].value.trim() !== ''; }); } // console.log('[checkIfTargetHasNote] 找到的批注:', annotation, '结果:', !!annotation); return !!annotation; } /** * 根据是否已高亮和是否有批注来更新上下文菜单选项的显示 * @param {boolean} isHighlighted - 是否已高亮 * @param {boolean} hasNote - 是否已有批注 */ function updateContextMenuOptions(isHighlighted, hasNote = false, isReadOnlyMode = false) { if (!annotationContextMenuElement) return; const highlightOption = annotationContextMenuElement.querySelector('[data-action="highlight-block"]') || annotationContextMenuElement.querySelector('[data-action="highlight-paragraph"]'); const removeHighlightOption = document.getElementById('remove-highlight-option'); const addNoteOption = document.getElementById('add-note-option'); const editNoteOption = document.getElementById('edit-note-option'); const copyContentOption = document.getElementById('copy-content-option'); const highlightActionsDivider = document.getElementById('highlight-actions-divider'); const noteActionsDivider = document.getElementById('note-actions-divider'); if (isReadOnlyMode) { if (highlightOption) highlightOption.style.display = 'none'; if (removeHighlightOption) removeHighlightOption.style.display = 'none'; if (addNoteOption) addNoteOption.style.display = 'none'; if (editNoteOption) editNoteOption.style.display = 'none'; if (copyContentOption) copyContentOption.style.display = 'none'; if (highlightActionsDivider) highlightActionsDivider.style.display = 'none'; if (noteActionsDivider) noteActionsDivider.style.display = 'none'; return; } // 放宽:只要存在非空选区即可高亮,内部会自动映射到子块/跨子块 let canHighlight = false; try { const sel = window.getSelection(); canHighlight = !!(sel && sel.rangeCount && !sel.getRangeAt(0).collapsed); } catch { canHighlight = false; } if (highlightOption) { highlightOption.style.display = canHighlight ? 'block' : 'none'; try { highlightOption.textContent = '高亮选中内容'; } catch { /* noop */ } } if (removeHighlightOption) removeHighlightOption.style.display = isHighlighted ? 'block' : 'none'; if (copyContentOption) { const sel = window.getSelection(); const hasRange = sel && sel.rangeCount && !sel.getRangeAt(0).collapsed; copyContentOption.style.display = hasRange ? 'block' : 'none'; } if (isHighlighted) { if (addNoteOption) addNoteOption.style.display = hasNote ? 'none' : 'block'; if (editNoteOption) editNoteOption.style.display = hasNote ? 'block' : 'none'; } else { if (addNoteOption) addNoteOption.style.display = 'none'; if (editNoteOption) editNoteOption.style.display = 'none'; } if (highlightActionsDivider) { highlightActionsDivider.style.display = isHighlighted ? 'block' : 'none'; } if (noteActionsDivider) { const noteOptionsVisible = (addNoteOption && addNoteOption.style.display === 'block') || (editNoteOption && editNoteOption.style.display === 'block'); noteActionsDivider.style.display = isHighlighted && noteOptionsVisible ? 'block' : 'none'; } } /** * 显示上下文菜单 * @param {number} x - x坐标 * @param {number} y - y坐标 */ function showContextMenu(x, y) { if (!annotationContextMenuElement) return; annotationContextMenuElement.style.left = x + 'px'; annotationContextMenuElement.style.top = y + 'px'; annotationContextMenuElement.classList.remove('context-menu-hidden'); annotationContextMenuElement.classList.add('context-menu-visible'); } /** * 隐藏上下文菜单并重置相关状态 */ function hideContextMenu() { if (!annotationContextMenuElement) return; annotationContextMenuElement.classList.remove('context-menu-visible'); annotationContextMenuElement.classList.add('context-menu-hidden'); // 重置由 history_detail.html 管理的全局变量 window.globalCurrentSelection = null; // window.globalCurrentTargetElement = null; // 作用减弱 window.globalCurrentHighlightStatus = false; } /** * 通用函数:从数据库中移除指定目标的批注。 * @param {string} docId - 文档ID。 * @param {string} [annotationId=null] - 可选的批注ID。 * @param {string} [targetIdentifier=null] - 目标元素的标识符 (blockIndex 或 subBlockId)。 * @param {string} contentIdentifier - 内容标识符。 * @param {'blockIndex'|'subBlockId'} identifierType - 标识符的类型。 */ async function removeAnnotationFromTarget(docId, annotationId = null, targetIdentifier = null, contentIdentifier, identifierType) { if (!window.data.annotations) { console.warn(`[批注逻辑] removeAnnotationFromTarget: window.data.annotations 未定义。`); return; } if (!annotationId && targetIdentifier === null) { console.error(`[批注逻辑] removeAnnotationFromTarget: 需要 annotationId 或 targetIdentifier。`); throw new Error('未指定要删除的批注 (无ID或目标标识符)。'); } // 增强日志:记录所有相关参数 console.log(`[批注逻辑] removeAnnotationFromTarget 参数: docId=${docId}, annotationId=${annotationId}, targetIdentifier=${targetIdentifier}, contentIdentifier=${contentIdentifier}, identifierType=${identifierType}`); // 记录当前所有批注的数量和类型 if (window.data.annotations) { console.log(`[批注逻辑] 当前批注总数: ${window.data.annotations.length}`); const typeCounts = {}; window.data.annotations.forEach(ann => { const type = ann.targetType || 'unknown'; typeCounts[type] = (typeCounts[type] || 0) + 1; }); console.log(`[批注逻辑] 批注类型统计:`, typeCounts); } let annotationsToRemove = []; if (annotationId) { // 通过ID查找批注 annotationsToRemove = window.data.annotations.filter(ann => ann.id === annotationId && ann.targetType === contentIdentifier); console.log(`[批注逻辑] 通过ID查找批注: ${annotationsToRemove.length}个匹配`); } else if (targetIdentifier !== null && identifierType) { // 通过目标标识符查找批注,增强类型比较 const targetIdStr = String(targetIdentifier).trim(); annotationsToRemove = window.data.annotations.filter(ann => { // 确保基本条件匹配 if (ann.targetType !== contentIdentifier) return false; if (!ann.target || !Array.isArray(ann.target.selector) || !ann.target.selector[0]) return false; // 获取选择器中的标识符,确保转换为字符串进行比较 const selectorId = ann.target.selector[0][identifierType]; if (selectorId === undefined) return false; const selectorIdStr = String(selectorId).trim(); // 记录详细的比较信息以便调试 const isMatch = selectorIdStr === targetIdStr; if (selectorIdStr === targetIdStr || Math.abs(Number(selectorIdStr) - Number(targetIdStr)) < 0.001) { console.log(`[批注逻辑] 找到匹配: ${selectorIdStr} == ${targetIdStr} (${identifierType})`); return true; } return false; }); console.log(`[批注逻辑] 通过${identifierType}查找批注: ${annotationsToRemove.length}个匹配 (目标值: ${targetIdStr})`); // 如果没有找到匹配,记录所有可能的值以便调试 if (annotationsToRemove.length === 0) { const allValues = window.data.annotations .filter(ann => ann.targetType === contentIdentifier && ann.target && ann.target.selector && ann.target.selector[0]) .map(ann => { const val = ann.target.selector[0][identifierType]; return val !== undefined ? String(val) : 'undefined'; }); console.log(`[批注逻辑] 当前所有${identifierType}值:`, allValues); } } if (annotationsToRemove.length === 0) { console.warn(`[批注逻辑] removeAnnotationFromTarget: 未找到要删除的批注。 ID: ${annotationId}, TargetID: ${targetIdentifier}, Type: ${identifierType}`); return; } console.log(`[批注逻辑] 将删除${annotationsToRemove.length}个批注:`, annotationsToRemove); for (const annotation of annotationsToRemove) { try { await deleteAnnotationFromDB(annotation.id); const index = window.data.annotations.findIndex(ann => ann.id === annotation.id); if (index > -1) { window.data.annotations.splice(index, 1); console.log(`[批注逻辑] 成功从内存中删除批注 ID: ${annotation.id}`); } else { console.warn(`[批注逻辑] 无法从内存中删除批注 ID: ${annotation.id} (未找到索引)`); } } catch (error) { console.error(`[批注逻辑] removeAnnotationFromTarget: 删除批注失败:`, error); throw error; } } } /** * 通用函数:为现有的已高亮目标添加或更新批注内容。 * @param {string} noteText - 批注内容。 * @param {string} docId - 文档ID。 * @param {string} [annotationId=null] - 可选的批注ID。 * @param {string} [targetIdentifier=null] - 目标元素的标识符 (blockIndex 或 subBlockId)。 * @param {string} contentIdentifier - 内容标识符。 * @param {'blockIndex'|'subBlockId'} identifierType - 标识符的类型。 */ async function addNoteToAnnotation(noteText, docId, annotationId = null, targetIdentifier = null, contentIdentifier, identifierType) { if (!window.data.annotations) { throw new Error('没有找到批注数据'); } if (!annotationId && targetIdentifier === null) { console.error(`[批注逻辑] addNoteToAnnotation: 需要 annotationId 或 targetIdentifier。`); throw new Error('未指定要添加批注的目标 (无ID或目标标识符)。'); } // 增强日志:记录所有相关参数 console.log(`[批注逻辑] addNoteToAnnotation 参数: docId=${docId}, annotationId=${annotationId}, targetIdentifier=${targetIdentifier}, contentIdentifier=${contentIdentifier}, identifierType=${identifierType}`); let existingAnnotation; if (annotationId) { // 通过ID查找批注 existingAnnotation = window.data.annotations.find(ann => ann.id === annotationId && ann.targetType === contentIdentifier && (ann.motivation === 'highlighting' || ann.motivation === 'commenting') ); console.log(`[批注逻辑] 通过ID查找批注进行添加/更新批注: ${existingAnnotation ? '找到' : '未找到'}`); } else if (targetIdentifier !== null && identifierType) { // 通过目标标识符查找批注,使用与其他函数一致的匹配逻辑 const targetIdStr = String(targetIdentifier).trim(); existingAnnotation = window.data.annotations.find(ann => { // 确保基本条件匹配 if (ann.targetType !== contentIdentifier) return false; if (!ann.target || !Array.isArray(ann.target.selector) || !ann.target.selector[0]) return false; if (!(ann.motivation === 'highlighting' || ann.motivation === 'commenting')) return false; // 获取选择器中的标识符,确保转换为字符串进行比较 const selectorId = ann.target.selector[0][identifierType]; if (selectorId === undefined) return false; const selectorIdStr = String(selectorId).trim(); // 使用与其他函数相同的比较逻辑 return selectorIdStr === targetIdStr || Math.abs(Number(selectorIdStr) - Number(targetIdStr)) < 0.001; }); console.log(`[批注逻辑] 通过${identifierType}查找批注进行添加/更新批注: ${existingAnnotation ? '找到' : '未找到'} (目标值: ${targetIdStr})`); } if (!existingAnnotation) { console.warn(`[批注逻辑] addNoteToAnnotation: 未找到对应的高亮批注。 ID: ${annotationId}, TargetID: ${targetIdentifier}, Type: ${identifierType}`); throw new Error('未找到对应的高亮批注进行批注操作'); } existingAnnotation.body = [{ type: 'TextualBody', value: noteText, format: 'text/plain', purpose: 'commenting' }]; existingAnnotation.modified = new Date().toISOString(); existingAnnotation.motivation = 'commenting'; try { await updateAnnotationInDB(existingAnnotation); console.log(`[批注逻辑] 成功更新批注 ID: ${existingAnnotation.id}`); // 新增:批注内容变动后立即刷新目标元素的title/class let targetElement = null; if (identifierType === 'subBlockId') { const containerId = contentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('.sub-block[data-sub-block-id="' + (existingAnnotation.target.selector[0].subBlockId || targetIdentifier) + '"]'); } } else if (identifierType === 'blockIndex') { const containerId = contentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('[data-block-index="' + (existingAnnotation.target.selector[0].blockIndex || targetIdentifier) + '"]'); } } if (targetElement && window.highlightBlockOrSubBlock) { window.highlightBlockOrSubBlock(targetElement, existingAnnotation, contentIdentifier, targetIdentifier, identifierType === 'subBlockId' ? 'subBlock' : 'block'); } } catch (error) { console.error(`[批注逻辑] addNoteToAnnotation: 更新批注失败:`, error); throw error; } } // 主初始化函数,由 history_detail.html 调用 function initAnnotationSystem() { annotationContextMenuElement = document.getElementById('custom-context-menu'); if (!annotationContextMenuElement) { console.error("未找到批注上下文菜单元素 ('custom-context-menu')!"); return; } // ========== 事件委托:只在 .container 上全局绑定一次 contextmenu ========== const mainContainer = document.querySelector('.container'); if (mainContainer) { if (mainContainer._annotationContextMenuBound) return; mainContainer._annotationContextMenuBound = true; mainContainer.addEventListener('contextmenu', function(event) { // 防呆:内容未加载完成时禁止右键 if (!window.contentReady) { alert('请等待内容加载完成后再右键区块。'); return; } // ===== 防重复触发机制 ===== if (this._contextMenuProcessing) { console.log('[跨子块检测] 事件正在处理中,跳过重复触发'); return; } this._contextMenuProcessing = true; // 延迟重置标志,避免快速重复触发 setTimeout(() => { this._contextMenuProcessing = false; }, 100); // ===== 新增:跨子块选择检测 ===== console.log('[跨子块检测] 开始检测跨子块选择...'); // 使用缓存获取所有子块(Phase 2.3 优化) let allSubBlocks = window.AnnotationDOMCache.getAllSubBlocks(); console.log('[跨子块检测] 页面上的子块总数:', allSubBlocks.length); if (allSubBlocks.length === 0) { console.log('[跨子块检测] ⚠️ 页面上没有找到任何子块!内容可能还没有分割。'); const blocks = document.querySelectorAll('[data-block-index]'); console.log('[跨子块检测] [data-block-index]元素数量:', blocks.length); if (blocks.length > 0 && window.SubBlockSegmenter && typeof window.SubBlockSegmenter.segment === 'function') { console.log('[跨子块检测] 触发自动分块(英文/中文标点)'); blocks.forEach(el => { try { window.SubBlockSegmenter.segment(el, el.dataset.blockIndex, true); } catch (e) { console.warn('[跨子块检测] 自动分块失败:', e); } }); // 自动分块后刷新缓存 allSubBlocks = window.AnnotationDOMCache.refresh().getAllSubBlocks(); console.log('[跨子块检测] 自动分块后 .sub-block数量:', allSubBlocks.length); } } else { console.log('[跨子块检测] 前5个子块ID:', Array.from(allSubBlocks).slice(0, 5).map(sb => sb.dataset.subBlockId)); } const crossBlockSelection = detectCrossBlockSelection(); console.log('[跨子块检测] 检测结果:', crossBlockSelection); if (crossBlockSelection.isCrossBlock) { console.log('[跨子块检测] 检测到跨子块选择,处理跨子块标注'); event.preventDefault(); // 阻止默认行为 return handleCrossBlockAnnotation(event, crossBlockSelection); } else { console.log('[跨子块检测] 未检测到跨子块选择,继续单子块处理'); } // 只处理 .sub-block 或 [data-block-index] 的右键 (仅在非跨子块情况下) let targetSubBlock = event.target.closest('.sub-block[data-sub-block-id]'); let targetBlock = event.target.closest('[data-block-index]'); // 优先:使用当前选区的起点子块作为目标,避免误选到上一段 try { const sel = window.getSelection(); if (sel && sel.rangeCount) { const r = sel.getRangeAt(0); if (!r.collapsed) { const startEl = r.startContainer.nodeType === Node.TEXT_NODE ? r.startContainer.parentElement : r.startContainer; const subFromSelection = startEl && startEl.closest ? startEl.closest('.sub-block[data-sub-block-id]') : null; if (subFromSelection) { targetSubBlock = subFromSelection; targetBlock = subFromSelection.closest('[data-block-index]') || targetBlock; } else if (!targetSubBlock) { // 若选区存在但所在段落尚未分段,则对该段落强制分段并定位子块 const blockEl = startEl && startEl.closest ? startEl.closest('[data-block-index]') : null; if (blockEl && window.SubBlockSegmenter && typeof window.SubBlockSegmenter.segment === 'function') { try { // 计算选区在块内的文本偏移 const getTextOffset = (elementNode, parentBlock) => { let offset = 0; const walker = document.createTreeWalker(parentBlock, NodeFilter.SHOW_TEXT, null, false); let n; while ((n = walker.nextNode())) { if (n === elementNode || n.parentElement === elementNode) break; offset += (n.textContent || '').length; } return offset; }; const preOffset = getTextOffset(r.startContainer, blockEl); window.SubBlockSegmenter.segment(blockEl, blockEl.dataset.blockIndex, true); // 在新子块中查找对应的位置 const subBlocks = blockEl.querySelectorAll('.sub-block[data-sub-block-id]'); let acc = 0; subBlocks.forEach(sb => { const L = (sb.textContent || '').length; if (targetSubBlock) return; if (preOffset >= acc && preOffset < acc + L) targetSubBlock = sb; acc += L; }); if (!targetSubBlock) { // 仍未定位到具体子块:使用块元素本身(虚拟子块)并兼容渲染器 if (!blockEl.dataset.subBlockId) { blockEl._virtualSubBlockId = blockEl.dataset.blockIndex + '.0'; blockEl.dataset.subBlockId = blockEl._virtualSubBlockId; } if (!blockEl.classList.contains('sub-block')) { blockEl.classList.add('sub-block'); } targetSubBlock = blockEl; } if (!targetBlock) targetBlock = blockEl; } catch (e) { /* ignore */ } } } } } } catch(e){ /* ignore */ } if (!targetSubBlock && !targetBlock) { console.log('[单子块检测] 右键目标不是子块或块级元素,忽略'); return; } // 新增:判断是否为只读视图 (分块对比模式) const isReadOnlyView = window.currentVisibleTabId === 'chunk-compare'; if (isReadOnlyView) { event.preventDefault(); hideContextMenu(); return; } let targetElementForAnnotation; let identifier, identifierType, blockIndexForContext = null, selectedTextForContext; let isOnlySubBlock = false; if (targetSubBlock) { targetElementForAnnotation = targetSubBlock; identifier = targetSubBlock.dataset.subBlockId; identifierType = 'subBlockId'; if (targetSubBlock.dataset.isOnlySubBlock === "true") { isOnlySubBlock = true; } const parentBlockElement = targetSubBlock.closest('[data-block-index]'); if (parentBlockElement) { blockIndexForContext = parentBlockElement.dataset.blockIndex; } } else if (targetBlock) { targetElementForAnnotation = targetBlock; identifier = targetBlock.dataset.blockIndex; identifierType = 'blockIndex'; blockIndexForContext = identifier; } else { hideContextMenu(); return; } const annotationId = targetElementForAnnotation.dataset.annotationId; // 优先采用当前选区文本 try { const sel = window.getSelection(); if (sel && sel.rangeCount && !sel.getRangeAt(0).collapsed) { selectedTextForContext = sel.toString(); } else { selectedTextForContext = targetElementForAnnotation.textContent; } } catch { selectedTextForContext = targetElementForAnnotation.textContent; } // 选区设置:仅使用用户当前选区(不再强制整块选中) let effectiveRange; try { const sel = window.getSelection(); if (sel && sel.rangeCount && !sel.getRangeAt(0).collapsed) { effectiveRange = sel.getRangeAt(0).cloneRange(); } } catch { /* noop */ } window.globalCurrentSelection = { text: selectedTextForContext, range: effectiveRange, annotationId: annotationId, targetElement: targetElementForAnnotation, contentIdentifierForSelection: window.globalCurrentContentIdentifier, [identifierType]: identifier, blockIndex: blockIndexForContext }; // Store context directly on the menu element annotationContextMenuElement.dataset.contextContentIdentifier = window.globalCurrentContentIdentifier; annotationContextMenuElement.dataset.contextTargetIdentifier = identifier; annotationContextMenuElement.dataset.contextIdentifierType = identifierType; if (annotationId) { annotationContextMenuElement.dataset.contextAnnotationId = annotationId; } else { delete annotationContextMenuElement.dataset.contextAnnotationId; } if (selectedTextForContext) { annotationContextMenuElement.dataset.contextSelectedText = selectedTextForContext; } else { delete annotationContextMenuElement.dataset.contextSelectedText; } if (isOnlySubBlock && identifierType === 'subBlockId') { annotationContextMenuElement.dataset.contextIsOnlySubBlock = "true"; } else { delete annotationContextMenuElement.dataset.contextIsOnlySubBlock; } if (blockIndexForContext) { annotationContextMenuElement.dataset.contextBlockIndex = blockIndexForContext; } else { delete annotationContextMenuElement.dataset.contextBlockIndex; } // 🔧 BUG FIX: 清除跨子块相关属性,避免单子块操作时误用旧的跨子块数据 delete annotationContextMenuElement.dataset.contextIsCrossBlock; delete annotationContextMenuElement.dataset.contextCrossBlockAnnotationId; delete annotationContextMenuElement.dataset.contextAffectedSubBlocks; console.log(`%c[AnnotationLogic ContxtMenu] Event triggered for container: ${mainContainer.id}, content type: ${window.globalCurrentContentIdentifier}`, 'color: blue; font-weight: bold;'); console.log(` Stored on menu - contentId: ${annotationContextMenuElement.dataset.contextContentIdentifier}, targetId: ${annotationContextMenuElement.dataset.contextTargetIdentifier}, type: ${annotationContextMenuElement.dataset.contextIdentifierType}, annId: ${annotationContextMenuElement.dataset.contextAnnotationId}, blockIdx: ${annotationContextMenuElement.dataset.contextBlockIndex}`); console.log(` Selected text stored on menu: ${(annotationContextMenuElement.dataset.contextSelectedText || '').substring(0,50)}...`); const isHighlighted = checkIfTargetIsHighlighted(annotationId, window.globalCurrentContentIdentifier, identifier, identifierType); const hasNote = checkIfTargetHasNote(annotationId, window.globalCurrentContentIdentifier, identifier, identifierType); console.log(` checkIfTargetIsHighlighted(...) returned: ${isHighlighted}`); console.log(` checkIfTargetHasNote(...) returned: ${hasNote}`); window.globalCurrentHighlightStatus = isHighlighted; // 仅在可高亮(跨子块或子块内存在非空选区)或点击已有高亮时显示菜单 let canHighlight = false; try { const sel = window.getSelection(); const hasSelection = sel && sel.rangeCount && !sel.getRangeAt(0).collapsed; console.log(`[调试] 选区检测: hasSelection=${hasSelection}, targetSubBlock=${!!targetSubBlock}, annotationId=${annotationId}`); if (hasSelection) { console.log(`[调试] 选中文本: "${sel.toString().substring(0, 50)}..."`); } canHighlight = !!(hasSelection && targetSubBlock); } catch(e) { console.warn('[调试] 选区检测失败:', e); canHighlight = false; } const clickedHighlighted = !!annotationId; console.log(`[调试] canHighlight=${canHighlight}, clickedHighlighted=${clickedHighlighted}`); if (!canHighlight && !clickedHighlighted) { console.log('[调试] ❌ 不显示菜单:既没有有效选区,也没有点击已有高亮'); hideContextMenu(); return; // 允许默认浏览器菜单 } updateContextMenuOptions(isHighlighted, hasNote, false); event.preventDefault(); // 仅在显示自定义菜单时阻止默认菜单 // 使用 clientX/clientY(相对于视口)配合 position: fixed showContextMenu(event.clientX, event.clientY); }, false); } // ...其余初始化逻辑... annotationContextMenuElement.addEventListener('click', async (event) => { let target = event.target; let action, color; // Prevent menu from closing itself if a menu item is clicked event.stopPropagation(); if (target.classList.contains('color-option')) { const parentLi = target.closest('li[data-action]'); if (parentLi) { action = parentLi.dataset.action; color = target.dataset.color; } } else { const li = target.closest('li[data-action]'); if (li) { action = li.dataset.action; } } if (!action) { hideContextMenu(); // If clicked on non-action area within menu, hide it. return; } // 更新:在分块对比模式下阻止所有指定操作 if (window.currentVisibleTabId === 'chunk-compare' && action && //确保 action 已定义 (action === 'highlight-block' || action === 'remove-highlight' || action === 'add-note' || action === 'edit-note' || action === 'copy-content')) { // 添加 copy-content console.warn(`[批注逻辑] 在分块对比模式下尝试执行操作 '${action}'。此操作应已被UI阻止。`); hideContextMenu(); return; // 阻止操作 } // ===== 新增:跨子块操作检测 ===== const isCrossBlockOperation = annotationContextMenuElement.dataset.contextIsCrossBlock === "true"; if (isCrossBlockOperation) { return handleCrossBlockMenuAction(action, color, event); } const docId = getQueryParam('id'); if (!docId) { alert('错误:无法获取文档ID。'); hideContextMenu(); return; } // Retrieve context from the menu's dataset let currentContentIdentifier = annotationContextMenuElement.dataset.contextContentIdentifier || (window.globalCurrentSelection && window.globalCurrentSelection.contentIdentifierForSelection) || window.globalCurrentContentIdentifier; // 兜底 let targetIdentifier = annotationContextMenuElement.dataset.contextTargetIdentifier || (window.globalCurrentSelection && (window.globalCurrentSelection.subBlockId || window.globalCurrentSelection.blockIndex)); let identifierType = annotationContextMenuElement.dataset.contextIdentifierType || (window.globalCurrentSelection && (window.globalCurrentSelection.subBlockId ? 'subBlockId' : 'blockIndex')); let targetAnnotationId = annotationContextMenuElement.dataset.contextAnnotationId || (window.globalCurrentSelection && window.globalCurrentSelection.annotationId); let originalSelectedText = annotationContextMenuElement.dataset.contextSelectedText || (window.globalCurrentSelection && window.globalCurrentSelection.text); if ((!(currentContentIdentifier && identifierType && targetIdentifier)) && (action === 'highlight-block' || action === 'add-note' || action === 'edit-note' || action === 'remove-highlight')) { console.log('context debug', {currentContentIdentifier, targetIdentifier, identifierType, targetAnnotationId, windowGlobal: window.globalCurrentSelection, windowGlobalContent: window.globalCurrentContentIdentifier}); alert('请重新右键点击目标区块后再操作。'); hideContextMenu(); return; } const hasValidContext = targetIdentifier && identifierType; if (!hasValidContext && (action === 'highlight-block' || action === 'add-note' || action === 'edit-note' || action === 'remove-highlight' || action === 'copy-content')) { alert('操作目标无效。请重新右键点击目标区块。'); console.error('[批注逻辑] Context menu action failed: targetIdentifier or identifierType from menu dataset is missing.'); hideContextMenu(); return; } let refreshNeeded = false; try { if (action === 'remove-highlight') { // identifierType is already from dataset await removeAnnotationFromTarget(docId, targetAnnotationId, targetIdentifier, currentContentIdentifier, identifierType); // 新增:只移除目标元素的高亮 let targetElement = null; if (identifierType === 'subBlockId') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('.sub-block[data-sub-block-id="' + targetIdentifier + '"]'); } } else if (identifierType === 'blockIndex') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('[data-block-index="' + targetIdentifier + '"]'); } } if (targetElement && window.removeHighlightFromBlockOrSubBlock) { window.removeHighlightFromBlockOrSubBlock(targetElement); } // 新增:局部刷新所有高亮,保证同步 if (typeof window.applyBlockAnnotations === 'function') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { window.applyBlockAnnotations(container, window.data.annotations, currentContentIdentifier); } } console.log(`${identifierType} 高亮已尝试取消`); refreshNeeded = false; // 不再全量刷新 } else if (action === 'add-note' || action === 'edit-note') { // identifierType is from dataset const isCurrentlyHighlighted = checkIfTargetIsHighlighted(targetAnnotationId, currentContentIdentifier, targetIdentifier, identifierType); if (!isCurrentlyHighlighted) { alert('只能对已高亮的区块/子区块操作批注。请先高亮。'); } else { let noteText; let currentNoteContent = ''; if (action === 'edit-note') { const existingAnnotation = window.data.annotations.find(a => a.targetType === currentContentIdentifier && (a.id === targetAnnotationId || (targetIdentifier && identifierType && a.target && a.target.selector && a.target.selector[0] && (a.target.selector[0][identifierType] === targetIdentifier || String(a.target.selector[0][identifierType]) === targetIdentifier) )) && a.body && a.body.length > 0 ); currentNoteContent = existingAnnotation && existingAnnotation.body[0] ? existingAnnotation.body[0].value : ''; noteText = prompt("编辑批注内容:", currentNoteContent); } else { // add-note noteText = prompt("请输入批注内容:", ""); } if (noteText === null) { /* User cancelled */ } else if (noteText.trim() === '') { alert('批注内容不能为空。'); } else { // 新增:同步 exact 字段 let annotationToUpdate = window.data.annotations.find(a => a.targetType === currentContentIdentifier && (a.id === targetAnnotationId || (targetIdentifier && identifierType && a.target && a.target.selector && a.target.selector[0] && (a.target.selector[0][identifierType] === targetIdentifier || String(a.target.selector[0][identifierType]) === targetIdentifier) )) ); if (annotationToUpdate && window.globalCurrentSelection && window.globalCurrentSelection.targetElement) { annotationToUpdate.target.selector[0].exact = window.globalCurrentSelection.targetElement.textContent.trim(); } await addNoteToAnnotation(noteText, docId, targetAnnotationId, targetIdentifier, currentContentIdentifier, identifierType); console.log(action === 'edit-note' ? `${identifierType} 批注已更新` : `批注已添加到现有 ${identifierType} 高亮`); refreshNeeded = true; } } } else if (action === 'highlight-block') { // 可选:禁止整块高亮(通过本地开关) try { const disableBlock = localStorage.getItem('DISABLE_BLOCK_HIGHLIGHT') === 'true'; if (disableBlock && identifierType === 'blockIndex') { alert('已禁用整块高亮,请在子块内选中要高亮的文本。'); hideContextMenu(); return; } } catch { /* noop */ } // 优先按当前选区的起点子块来高亮,避免“跳到上面一段” try { const sel = window.getSelection(); if (sel && sel.rangeCount) { const r = sel.getRangeAt(0); if (!r.collapsed) { const startEl = r.startContainer.nodeType === Node.TEXT_NODE ? r.startContainer.parentElement : r.startContainer; const subFromSelection = startEl && startEl.closest ? startEl.closest('.sub-block[data-sub-block-id]') : null; if (subFromSelection) { identifierType = 'subBlockId'; identifier = subFromSelection.dataset.subBlockId; targetIdentifier = identifier; targetElementForAnnotation = subFromSelection; } } } } catch { /* ignore */ } // 允许未选颜色,默认黄色 if (!color) { color = 'yellow'; } { // 预判是否为子块内片段选择 let isSubBlockRange = false; try { isSubBlockRange = (identifierType === 'subBlockId' && window.globalCurrentSelection && window.globalCurrentSelection.range && window.globalCurrentSelection.targetElement); } catch { isSubBlockRange = false; } // ====== 修正:高亮保存前去重,保证唯一性 ====== // 先查找所有同 target 的 annotation const duplicateAnnotations = window.data.annotations.filter(ann => ann.targetType === currentContentIdentifier && ann.target && ann.target.selector && ann.target.selector[0] && (ann.target.selector[0][identifierType] === targetIdentifier || String(ann.target.selector[0][identifierType]) === targetIdentifier) && (ann.motivation === 'highlighting' || ann.motivation === 'commenting') ); // 子块内片段:允许同一子块多段并存,不做去重/合并 let existingAnnotationForTarget = isSubBlockRange ? null : duplicateAnnotations[0]; if (!isSubBlockRange) { // 如果有多个,移除多余的,只保留第一个(仅限非片段场景) if (duplicateAnnotations.length > 1) { for (let i = 1; i < duplicateAnnotations.length; i++) { await removeAnnotationFromTarget(docId, duplicateAnnotations[i].id, targetIdentifier, currentContentIdentifier, identifierType); } } } if (existingAnnotationForTarget) { existingAnnotationForTarget.highlightColor = color; existingAnnotationForTarget.modified = new Date().toISOString(); if (existingAnnotationForTarget.motivation !== 'commenting') { existingAnnotationForTarget.motivation = 'highlighting'; } // 如果是单子块并且存在选区,则转换/更新为区间选择 if (identifierType === 'subBlockId' && window.globalCurrentSelection && window.globalCurrentSelection.range && window.globalCurrentSelection.targetElement) { try { const el = window.globalCurrentSelection.targetElement; const selRange = window.globalCurrentSelection.range; const isInFormula = (n) => { let p = n && (n.nodeType === Node.TEXT_NODE ? n.parentElement : n); while (p) { if (p.classList && ( p.classList.contains('katex') || p.classList.contains('katex-display') || p.classList.contains('katex-inline') || p.classList.contains('reference-citation') // 保护引用链接 )) return true; p = p.parentElement; } return false; }; const fragTextLenExclFormula = (frag) => { const walker = document.createTreeWalker(frag, NodeFilter.SHOW_TEXT, null, false); let len = 0, node; while ((node = walker.nextNode())) { if (!isInFormula(node)) len += (node.nodeValue || '').length; } return len; }; const calcOffset = (endNode, endOffset) => { const r = document.createRange(); r.selectNodeContents(el); r.setEnd(endNode, endOffset); return fragTextLenExclFormula(r.cloneContents()); }; const sOff = calcOffset(selRange.startContainer, selRange.startOffset); const eOff = calcOffset(selRange.endContainer, selRange.endOffset); const startOffset = Math.max(0, Math.min(sOff, eOff)); const endOffset = Math.max(0, Math.max(sOff, eOff)); const exactSel = (window.globalCurrentSelection.text || '').trim(); if (!existingAnnotationForTarget.target.selector[0] || existingAnnotationForTarget.target.selector[0].type !== 'SubBlockRangeSelector') { existingAnnotationForTarget.target.selector[0] = { type: 'SubBlockRangeSelector', subBlockId: targetIdentifier }; } existingAnnotationForTarget.target.selector[0].startOffset = startOffset; existingAnnotationForTarget.target.selector[0].endOffset = endOffset; if (exactSel) existingAnnotationForTarget.target.selector[0].exact = exactSel; } catch (e) { /* ignore and fallback */ } } else if (window.globalCurrentSelection && window.globalCurrentSelection.targetElement) { // 同步 exact 字段(整块高亮) existingAnnotationForTarget.target.selector[0].exact = window.globalCurrentSelection.targetElement.textContent.trim(); } await updateAnnotationInDB(existingAnnotationForTarget); // 新增:只高亮目标元素 let targetElement = null; if (identifierType === 'subBlockId') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('.sub-block[data-sub-block-id="' + targetIdentifier + '"]'); } } else if (identifierType === 'blockIndex') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('[data-block-index="' + targetIdentifier + '"]'); } } if (targetElement && window.highlightBlockOrSubBlock) { window.highlightBlockOrSubBlock(targetElement, existingAnnotationForTarget, currentContentIdentifier, targetIdentifier, identifierType === 'subBlockId' ? 'subBlock' : 'block'); } console.log(`${identifierType} 高亮颜色已更新:`, existingAnnotationForTarget); refreshNeeded = true; } else { const newAnnotation = { '@context': 'http://www.w3.org/ns/anno.jsonld', id: 'urn:uuid:' + _page_generateUUID(), type: 'Annotation', motivation: 'highlighting', created: new Date().toISOString(), docId: docId, targetType: currentContentIdentifier, highlightColor: color, target: { source: docId, selector: [{ type: identifierType === 'subBlockId' ? 'SubBlockSelector' : 'BlockSelector', }] }, body: [] }; newAnnotation.target.selector[0][identifierType] = targetIdentifier; // 单子块 + 存在选区:改为区间选择 if (identifierType === 'subBlockId' && window.globalCurrentSelection && window.globalCurrentSelection.range && window.globalCurrentSelection.targetElement) { try { const el = window.globalCurrentSelection.targetElement; const selRange = window.globalCurrentSelection.range; const isInFormula = (n) => { let p = n && (n.nodeType === Node.TEXT_NODE ? n.parentElement : n); while (p) { if (p.classList && ( p.classList.contains('katex') || p.classList.contains('katex-display') || p.classList.contains('katex-inline') || p.classList.contains('reference-citation') // 保护引用链接 )) return true; p = p.parentElement; } return false; }; const fragTextLenExclFormula = (frag) => { const walker = document.createTreeWalker(frag, NodeFilter.SHOW_TEXT, null, false); let len = 0, node; while ((node = walker.nextNode())) { if (!isInFormula(node)) len += (node.nodeValue || '').length; } return len; }; const calcOffset = (endNode, endOffset) => { const r = document.createRange(); r.selectNodeContents(el); r.setEnd(endNode, endOffset); return fragTextLenExclFormula(r.cloneContents()); }; const sOff = calcOffset(selRange.startContainer, selRange.startOffset); const eOff = calcOffset(selRange.endContainer, selRange.endOffset); const startOffset = Math.max(0, Math.min(sOff, eOff)); const endOffset = Math.max(0, Math.max(sOff, eOff)); const exactSel = (window.globalCurrentSelection.text || '').trim(); newAnnotation.target.selector[0] = { type: 'SubBlockRangeSelector', subBlockId: targetIdentifier, startOffset: startOffset, endOffset: endOffset, exact: exactSel }; } catch (e) { // 回退整块 if (window.globalCurrentSelection && window.globalCurrentSelection.targetElement) { newAnnotation.target.selector[0].exact = window.globalCurrentSelection.targetElement.textContent.trim(); } else if (originalSelectedText) { newAnnotation.target.selector[0].exact = originalSelectedText; } } } else { // 新建时写入 exact(整块) if (window.globalCurrentSelection && window.globalCurrentSelection.targetElement) { newAnnotation.target.selector[0].exact = window.globalCurrentSelection.targetElement.textContent.trim(); } else if (originalSelectedText) { newAnnotation.target.selector[0].exact = originalSelectedText; } } const contextBlockIndex = annotationContextMenuElement.dataset.contextBlockIndex; if (identifierType === 'subBlockId' && contextBlockIndex) { newAnnotation.target.selector[0].blockIndex = contextBlockIndex; } await saveAnnotationToDB(newAnnotation); if (!window.data.annotations) window.data.annotations = []; window.data.annotations.push(newAnnotation); // 新增:只高亮目标元素 let targetElement = null; if (identifierType === 'subBlockId') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('.sub-block[data-sub-block-id="' + targetIdentifier + '"]'); } } else if (identifierType === 'blockIndex') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container) { targetElement = container.querySelector('[data-block-index="' + targetIdentifier + '"]'); } } if (targetElement && window.highlightBlockOrSubBlock) { window.highlightBlockOrSubBlock(targetElement, newAnnotation, currentContentIdentifier, targetIdentifier, identifierType === 'subBlockId' ? 'subBlock' : 'block'); } refreshNeeded = true; console.log(`新 ${identifierType} 高亮已保存:`, newAnnotation); } refreshNeeded = false; // 不再全量刷新 } } else if (action === 'copy-content') { let textToCopy = originalSelectedText; // Default to textContent const contextBlockIndex = annotationContextMenuElement.dataset.contextBlockIndex; // 唯一子块判断逻辑修正 if (identifierType === 'blockIndex' && currentContentIdentifier && targetIdentifier) { const blockIndex = parseInt(targetIdentifier, 10); if (!isNaN(blockIndex) && window.currentBlockTokensForCopy && window.currentBlockTokensForCopy[currentContentIdentifier] && window.currentBlockTokensForCopy[currentContentIdentifier][blockIndex] && typeof window.currentBlockTokensForCopy[currentContentIdentifier][blockIndex].raw === 'string') { textToCopy = window.currentBlockTokensForCopy[currentContentIdentifier][blockIndex].raw; console.log(`[批注逻辑] 复制块级内容: 使用来自 currentBlockTokensForCopy 的原始 Markdown (块索引: ${blockIndex})。`); } else { console.warn(`[批注逻辑] 复制块级内容: 无法从 currentBlockTokensForCopy 获取原始 Markdown (块索引: ${blockIndex}),回退到 textContent。`); } } else if (identifierType === 'subBlockId' && currentContentIdentifier && contextBlockIndex) { // 统计 annotation 里所有属于该父块的唯一子块 const parentBlockIndex = parseInt(contextBlockIndex, 10); const allSubBlockIds = window.data.annotations .map(a => a.target && a.target.selector && a.target.selector[0] && a.target.selector[0].subBlockId) .filter(id => id && id.startsWith(`${parentBlockIndex}.`)); const uniqueSubBlockIds = Array.from(new Set(allSubBlockIds)); if (uniqueSubBlockIds.length === 1 && window.currentBlockTokensForCopy && window.currentBlockTokensForCopy[currentContentIdentifier] && window.currentBlockTokensForCopy[currentContentIdentifier][parentBlockIndex] && typeof window.currentBlockTokensForCopy[currentContentIdentifier][parentBlockIndex].raw === 'string') { textToCopy = window.currentBlockTokensForCopy[currentContentIdentifier][parentBlockIndex].raw; console.log(`[批注逻辑] 复制唯一的子块: 使用其父块的原始 Markdown (父块索引: ${parentBlockIndex})。`); } else { // 不是唯一子块,或无法获取父块内容,回退到子块的 textContent console.log(`[批注逻辑] 复制子块 (非唯一或无父块信息) 或其他内容: 使用 textContent。`); // textToCopy remains originalSelectedText (sub-block's textContent) } } else { // 其它情况 console.log(`[批注逻辑] 复制子块 (非唯一或无父块信息) 或其他内容: 使用 textContent。`); // textToCopy remains originalSelectedText (textContent) } if (!textToCopy) { // originalSelectedText could be empty if target has no text alert('没有可选择的内容进行复制。'); } else { navigator.clipboard.writeText(textToCopy) .then(() => { console.log(`文本已复制 (来源: ${identifierType === 'blockIndex' ? '原始Markdown或textContent' : 'textContent'}): ${String(textToCopy).substring(0,50)}...`); // alert('内容已复制!'); // Optional }) .catch(err => { console.error(`复制失败:`, err); alert('复制内容失败。'); }); } } } catch (error) { console.error(`[批注系统] 操作 '${action}' 失败:`, error); alert(`操作失败: ${error.message}`); } finally { hideContextMenu(); // Always hide menu after action or error if (refreshNeeded) { // ========== 优化:只局部刷新高亮和批注事件 ========== // 只在 OCR/translation tab 下局部刷新,不再全量 showTab const tab = window.currentVisibleTabId; let containerId = null; let contentIdentifier = null; if (tab === 'ocr') { containerId = 'ocr-content-wrapper'; contentIdentifier = 'ocr'; } else if (tab === 'translation') { containerId = 'translation-content-wrapper'; contentIdentifier = 'translation'; } if (containerId && typeof window.applyBlockAnnotations === 'function') { const container = document.getElementById(containerId); if (container) { window.applyBlockAnnotations(container, window.data.annotations, contentIdentifier); } } if (containerId && typeof window.addAnnotationListenersToContainer === 'function') { window.addAnnotationListenersToContainer(containerId, contentIdentifier); } // Dock/TOC统计也可局部刷新(可选) if (window.DockLogic && typeof window.DockLogic.updateStats === 'function') { window.DockLogic.updateStats(window.data, window.currentVisibleTabId); } if (typeof window.refreshTocList === 'function') { window.refreshTocList(); } if(typeof window.updateReadingProgress === 'function') window.updateReadingProgress(); // ===================================================== // 只有在内容结构变化时才需要全量 showTab // if (typeof window.showTab === 'function' && window.currentVisibleTabId) { // const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; // await Promise.resolve(window.showTab(window.currentVisibleTabId)); // requestAnimationFrame(() => { // document.documentElement.scrollTop = document.body.scrollTop = currentScroll; // if(typeof window.updateReadingProgress === 'function') window.updateReadingProgress(); // }); // } else { // console.warn("[批注系统] window.showTab 或 window.currentVisibleTabId 不可用,无法自动刷新视图。"); // } } } }); document.addEventListener('click', (event) => { if (annotationContextMenuElement && annotationContextMenuElement.classList.contains('context-menu-visible') && !annotationContextMenuElement.contains(event.target)) { if (event.target.classList.contains('color-option')) return; hideContextMenu(); } }); // 额外:滚动/窗口变化/Esc 时隐藏菜单,避免“菜单残留” try { document.addEventListener('scroll', hideContextMenu, true); window.addEventListener('resize', hideContextMenu); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideContextMenu(); }, true); } catch { /* noop */ } // console.log("[批注系统] 事件监听器已添加 (子块/块级模式)。"); } // ===== 新增:跨子块选择检测函数 ===== function detectCrossBlockSelection() { const selection = window.getSelection(); console.log('[跨子块检测] 当前选区:', selection); console.log('[跨子块检测] 选区范围数:', selection.rangeCount); if (!selection.rangeCount) { console.log('[跨子块检测] 没有选区范围'); return { isCrossBlock: false }; } const range = selection.getRangeAt(0); console.log('[跨子块检测] 选区范围:', range); console.log('[跨子块检测] 选区是否折叠:', range.collapsed); console.log('[跨子块检测] 选中文本:', selection.toString()); if (range.collapsed) { console.log('[跨子块检测] 选区已折叠,不是有效选择'); return { isCrossBlock: false }; } // 检测选择范围是否跨越多个子块 const startContainer = range.startContainer; const endContainer = range.endContainer; console.log('[跨子块检测] 开始容器:', startContainer); console.log('[跨子块检测] 结束容器:', endContainer); // 辅助函数:获取元素在父元素中的文本偏移(忽略公式内部文本) const getTextOffsetInElement = (element, parentElement) => { let offset = 0; const isFormulaNode = (n) => { let p = n && (n.nodeType === Node.TEXT_NODE ? n.parentElement : n); while (p) { if (p.classList && (p.classList.contains('katex') || p.classList.contains('katex-display') || p.classList.contains('katex-inline'))) return true; p = p.parentElement; } return false; }; const walker = document.createTreeWalker(parentElement, NodeFilter.SHOW_TEXT, null, false); let node; while ((node = walker.nextNode())) { if (node === element || node.parentElement === element) break; if (!isFormulaNode(node)) offset += (node.textContent || '').length; } return offset; }; // 辅助函数:根据文本偏移找到对应的子块 const findSubBlockByTextOffset = (blockElement, textOffset) => { const subBlocks = blockElement.querySelectorAll('.sub-block[data-sub-block-id]'); let currentOffset = 0; for (const subBlock of subBlocks) { const subBlockTextLength = subBlock.textContent.length; if (textOffset >= currentOffset && textOffset < currentOffset + subBlockTextLength) { return subBlock; } currentOffset += subBlockTextLength; } return null; }; // 改进:更准确地找到包含的子块或块级元素 const findParentSubBlock = (node, debugPrefix = '') => { // 如果是文本节点,从父元素开始查找 let element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; // 若起点在公式内部,先提升到公式容器,以保证后续最近子块判定稳定 const formulaContainer = element.closest && element.closest('.katex, .katex-display, .katex-inline'); if (formulaContainer) { element = formulaContainer; } // 查找所属的块级元素,用于调试 const blockElement = element.closest('[data-block-index]'); console.log(`[跨子块检测] ${debugPrefix}查找子块,起始元素:`, element); console.log(`[跨子块检测] ${debugPrefix}元素标签:`, element.tagName); console.log(`[跨子块检测] ${debugPrefix}所属段落:`, blockElement?.dataset?.blockIndex || '未找到'); // 首先查找最近的子块 const subBlock = element.closest('.sub-block[data-sub-block-id]'); console.log(`[跨子块检测] ${debugPrefix}找到的子块:`, subBlock?.dataset?.subBlockId || 'null'); if (subBlock) { return subBlock; } // 如果没找到子块,查找块级元素 console.log(`[跨子块检测] ${debugPrefix}找到的块级元素:`, blockElement); if (blockElement) { // 检查这个块是否已经被分段成子块 const childSubBlocks = blockElement.querySelectorAll('.sub-block[data-sub-block-id]'); console.log('[跨子块检测] 块级元素的子块数量:', childSubBlocks.length); if (childSubBlocks.length > 0) { // 如果有子块,需要确定具体是哪个子块 // 根据selection的位置来判断 const textOffset = getTextOffsetInElement(element, blockElement); const targetSubBlock = findSubBlockByTextOffset(blockElement, textOffset); console.log('[跨子块检测] 根据文本偏移找到的子块:', targetSubBlock); return targetSubBlock || childSubBlocks[0]; // 如果找不到就返回第一个 } else { // 如果没有子块,优先尝试自动分段(支持英文) if (window.SubBlockSegmenter && typeof window.SubBlockSegmenter.segment === 'function') { try { console.log('[跨子块检测] 块级元素未分段,触发针对该块的自动分段 (force=true)'); // 在分段之前先计算选区在该块内的文本偏移 const preTextOffset = getTextOffsetInElement(element, blockElement); window.SubBlockSegmenter.segment(blockElement, blockElement.dataset.blockIndex, true); const childAfter = blockElement.querySelectorAll('.sub-block[data-sub-block-id]'); console.log('[跨子块检测] 自动分段后子块数量:', childAfter.length); if (childAfter.length > 0) { // 移除之前可能打在块上的虚拟 subBlockId,避免与真实子块冲突 if (blockElement._virtualSubBlockId) delete blockElement._virtualSubBlockId; if (blockElement.dataset && blockElement.dataset.subBlockId) delete blockElement.dataset.subBlockId; // 使用分段前计算的偏移,映射到具体子块 const targetSubBlock2 = findSubBlockByTextOffset(blockElement, preTextOffset); console.log('[跨子块检测] 自动分段后根据偏移找到的子块:', targetSubBlock2); return targetSubBlock2 || childAfter[0]; } } catch (e) { console.warn('[跨子块检测] 单块自动分段失败:', e); } } // 仍无子块,创建虚拟子块标识 console.log(`[跨子块检测] ${debugPrefix}块级元素未分段,创建虚拟子块标识`); // 改进:确保虚拟子块ID的唯一性 const proposedId = blockElement.dataset.blockIndex + '.0'; // 检查是否已经被标记过(避免重复标记) if (!blockElement.dataset.subBlockId) { blockElement._virtualSubBlockId = proposedId; blockElement.dataset.subBlockId = proposedId; console.log(`[跨子块检测] ${debugPrefix}创建虚拟子块ID: ${proposedId}`); } else { console.log(`[跨子块检测] ${debugPrefix}块级元素已有子块ID: ${blockElement.dataset.subBlockId}`); } return blockElement; } } // 调试:查看父元素层次 let parent = element; let level = 0; while (parent && level < 5) { console.log(`[跨子块检测] 父元素层次${level}:`, parent.tagName, parent.className, parent.dataset); parent = parent.parentElement; level++; } return null; }; const startSubBlock = findParentSubBlock(startContainer, '开始容器-'); const endSubBlock = findParentSubBlock(endContainer, '结束容器-'); console.log('[跨子块检测] 开始子块:', startSubBlock); console.log('[跨子块检测] 结束子块:', endSubBlock); console.log('[跨子块检测] 开始子块ID:', startSubBlock?.dataset?.subBlockId); console.log('[跨子块检测] 结束子块ID:', endSubBlock?.dataset?.subBlockId); if (!startSubBlock || !endSubBlock) { console.log('[跨子块检测] 找不到开始或结束子块'); return { isCrossBlock: false }; } // 比较子块标识符而不是DOM元素 const startId = startSubBlock.dataset.subBlockId || startSubBlock._virtualSubBlockId; const endId = endSubBlock.dataset.subBlockId || endSubBlock._virtualSubBlockId; console.log('[跨子块检测] 开始子块ID:', startId); console.log('[跨子块检测] 结束子块ID:', endId); // 改进:更严格的跨子块判断逻辑 if (startId !== endId) { // 跨子块选择 console.log('[跨子块检测] ✅ 检测到跨子块选择!'); const affectedSubBlocks = getSubBlocksInRange(range, startSubBlock, endSubBlock); console.log('[跨子块检测] 影响的子块:', affectedSubBlocks); return { isCrossBlock: true, startSubBlock: startSubBlock, endSubBlock: endSubBlock, affectedSubBlocks: affectedSubBlocks, selectedText: selection.toString(), range: range }; } // 额外检查:即使子块ID相同,也要检查是否真的是同一个DOM元素 if (startSubBlock !== endSubBlock) { console.log('[跨子块检测] ✅ 检测到跨DOM元素选择(子块ID相同但DOM不同)!'); console.log('[跨子块检测] 开始DOM:', startSubBlock); console.log('[跨子块检测] 结束DOM:', endSubBlock); // 这种情况说明有问题,但仍然按跨子块处理 const affectedSubBlocks = getSubBlocksInRange(range, startSubBlock, endSubBlock); console.log('[跨子块检测] 影响的子块:', affectedSubBlocks); return { isCrossBlock: true, startSubBlock: startSubBlock, endSubBlock: endSubBlock, affectedSubBlocks: affectedSubBlocks, selectedText: selection.toString(), range: range }; } console.log('[跨子块检测] 选择在同一个子块内'); return { isCrossBlock: false }; } // ===== 新增:获取范围内的所有子块 ===== function getSubBlocksInRange(range, startSubBlock, endSubBlock) { const subBlocks = []; const commonAncestor = range.commonAncestorContainer; const container = commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentElement : commonAncestor; console.log('[获取范围内子块] 公共祖先容器:', container); console.log('[获取范围内子块] 开始子块:', startSubBlock); console.log('[获取范围内子块] 结束子块:', endSubBlock); // 获取开始和结束子块的ID const startId = startSubBlock.dataset.subBlockId || startSubBlock._virtualSubBlockId; const endId = endSubBlock.dataset.subBlockId || endSubBlock._virtualSubBlockId; console.log('[获取范围内子块] 开始子块ID:', startId); console.log('[获取范围内子块] 结束子块ID:', endId); // 首先确保包含开始和结束子块 if (startId) { subBlocks.push({ element: startSubBlock, subBlockId: startId, text: startSubBlock.textContent || '', isFullySelected: false, isVirtual: !startSubBlock.classList.contains('sub-block') }); console.log('[获取范围内子块] 添加开始子块:', startId); } if (endId && endId !== startId) { subBlocks.push({ element: endSubBlock, subBlockId: endId, text: endSubBlock.textContent || '', isFullySelected: false, isVirtual: !endSubBlock.classList.contains('sub-block') }); console.log('[获取范围内子块] 添加结束子块:', endId); } // 查找中间的子块(优先真实子块,否则按块级虚拟子块) const allSubBlocks = container.querySelectorAll('.sub-block[data-sub-block-id]'); console.log('[获取范围内子块] 找到的真实子块数量:', allSubBlocks.length); if (allSubBlocks.length > 0) { // 使用更准确的范围检测:先添加所有真实子块 for (const subBlock of allSubBlocks) { const subBlockId = subBlock.dataset.subBlockId; // 跳过已经添加的开始和结束子块 if (subBlockId === startId || subBlockId === endId) { continue; } // 检查子块是否在选择范围内 if (range.intersectsNode(subBlock)) { subBlocks.push({ element: subBlock, subBlockId: subBlockId, text: subBlock.textContent || '', isFullySelected: range.containsNode ? range.containsNode(subBlock) : false }); console.log('[获取范围内子块] 添加中间子块(真实):', subBlockId); } } // 同时补充:对范围内“没有真实子块”的段落,创建虚拟子块,避免中间段落遗漏 const top = container.closest && (container.closest('#ocr-content-wrapper, #translation-content-wrapper') || container.closest('[data-block-index]')?.parentElement) || document; const allBlocksInside = top.querySelectorAll('[data-block-index]'); const startBlockEl = startSubBlock.closest('[data-block-index]') || startSubBlock; const endBlockEl = endSubBlock.closest('[data-block-index]') || endSubBlock; const startIdxNum = parseInt(startBlockEl.dataset.blockIndex, 10); const endIdxNum = parseInt(endBlockEl.dataset.blockIndex, 10); const lowIdx = Math.min(startIdxNum, endIdxNum); const highIdx = Math.max(startIdxNum, endIdxNum); allBlocksInside.forEach(blockEl => { const bi = parseInt(blockEl.dataset.blockIndex, 10); if (isNaN(bi) || bi < lowIdx || bi > highIdx) return; if (!range.intersectsNode(blockEl)) return; const childSbs = blockEl.querySelectorAll('.sub-block[data-sub-block-id]'); const hasRealSubBlocks = childSbs.length > 0; const hasAnyAdded = subBlocks.some(sb => sb.subBlockId && String(sb.subBlockId).startsWith(String(bi) + '.')); const isStartOrEnd = (String(bi) + '.0' === startId) || (String(bi) + '.0' === endId); if (!hasRealSubBlocks && !hasAnyAdded) { // 为没有真实子块的段落创建虚拟子块 const virtualId = String(bi) + '.0'; if (!isStartOrEnd) { blockEl._virtualSubBlockId = virtualId; blockEl.dataset.subBlockId = virtualId; } subBlocks.push({ element: blockEl, subBlockId: virtualId, text: blockEl.textContent || '', isFullySelected: range.containsNode ? range.containsNode(blockEl) : false, isVirtual: true }); console.log('[获取范围内子块] 添加中间子块(虚拟,无真实子块的段落):', virtualId); } }); } else { // 没有真实子块:按块级元素范围生成虚拟子块,确保中间块不会漏掉 console.log('[获取范围内子块] 无真实子块,采用块级虚拟子块遍历'); // 尝试找到更高的容器(如 ocr/translation 包裹) let topContainer = container.closest && (container.closest('#ocr-content-wrapper, #translation-content-wrapper') || container.closest('[data-block-index]')?.parentElement) || document; const allBlocks = topContainer.querySelectorAll('[data-block-index]'); console.log('[获取范围内子块] 块级元素数量:', allBlocks.length); // 获取起止 blockIndex const startBlockEl = startSubBlock.closest('[data-block-index]') || startSubBlock; const endBlockEl = endSubBlock.closest('[data-block-index]') || endSubBlock; const startIdx = parseInt(startBlockEl.dataset.blockIndex, 10); const endIdx = parseInt(endBlockEl.dataset.blockIndex, 10); const low = Math.min(startIdx, endIdx); const high = Math.max(startIdx, endIdx); allBlocks.forEach(blockEl => { const bi = parseInt(blockEl.dataset.blockIndex, 10); if (isNaN(bi) || bi < low || bi > high) return; if (!range.intersectsNode(blockEl)) return; // 如果已有真实子块(某些页面后续会动态分割),优先真实子块 const subBlocksOfBlock = blockEl.querySelectorAll('.sub-block[data-sub-block-id]'); if (subBlocksOfBlock.length > 0) { subBlocksOfBlock.forEach(sb => { const id = sb.dataset.subBlockId; if (id === startId || id === endId) return; subBlocks.push({ element: sb, subBlockId: id, text: sb.textContent || '', isFullySelected: range.containsNode ? range.containsNode(sb) : false }); console.log('[获取范围内子块] 添加中间子块(真实):', id); }); } else { // 创建虚拟子块ID:blockIndex.0 const virtualId = blockEl.dataset.blockIndex + '.0'; if (virtualId === startId || virtualId === endId) return; blockEl._virtualSubBlockId = virtualId; blockEl.dataset.subBlockId = virtualId; subBlocks.push({ element: blockEl, subBlockId: virtualId, text: blockEl.textContent || '', isFullySelected: range.containsNode ? range.containsNode(blockEl) : false, isVirtual: true }); console.log('[获取范围内子块] 添加中间子块(虚拟):', virtualId); } }); } // 去重 const uniqueMap = new Map(); subBlocks.forEach(sb => { if (!uniqueMap.has(sb.subBlockId)) uniqueMap.set(sb.subBlockId, sb); }); let uniqueSubBlocks = Array.from(uniqueMap.values()); // 按文档顺序排序,确保从起点到终点连续 uniqueSubBlocks.sort((a, b) => { if (a.element === b.element) return 0; const pos = a.element.compareDocumentPosition(b.element); if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); console.log('[获取范围内子块] 最终结果(文档顺序):', uniqueSubBlocks); return uniqueSubBlocks; } // ===== 新增:处理跨子块标注 ===== function handleCrossBlockAnnotation(event, crossBlockInfo) { event.preventDefault(); console.log(`[跨子块标注] 检测到跨子块选择,涉及 ${crossBlockInfo.affectedSubBlocks.length} 个子块`); // 新增:判断是否为只读视图 (分块对比模式) const isReadOnlyView = window.currentVisibleTabId === 'chunk-compare'; if (isReadOnlyView) { hideContextMenu(); return; } // 生成跨子块标注ID const crossBlockAnnotationId = 'cross-' + _page_generateUUID(); // 设置全局选择信息 window.globalCurrentSelection = { text: crossBlockInfo.selectedText, range: crossBlockInfo.range.cloneRange(), isCrossBlock: true, crossBlockAnnotationId: crossBlockAnnotationId, affectedSubBlocks: crossBlockInfo.affectedSubBlocks, startSubBlock: crossBlockInfo.startSubBlock, endSubBlock: crossBlockInfo.endSubBlock, contentIdentifierForSelection: window.globalCurrentContentIdentifier, targetElement: crossBlockInfo.startSubBlock // 使用起始子块作为代表 }; // 在上下文菜单上存储信息 annotationContextMenuElement.dataset.contextContentIdentifier = window.globalCurrentContentIdentifier; annotationContextMenuElement.dataset.contextIsCrossBlock = "true"; annotationContextMenuElement.dataset.contextCrossBlockAnnotationId = crossBlockAnnotationId; annotationContextMenuElement.dataset.contextSelectedText = crossBlockInfo.selectedText; annotationContextMenuElement.dataset.contextAffectedSubBlocks = JSON.stringify( crossBlockInfo.affectedSubBlocks.map(sb => sb.subBlockId) ); // 检查是否已经有跨子块标注 const isHighlighted = checkCrossBlockHighlight(crossBlockInfo.affectedSubBlocks); const hasNote = checkCrossBlockNote(crossBlockInfo.affectedSubBlocks); console.log(`[跨子块标注] 高亮状态: ${isHighlighted}, 有批注: ${hasNote}`); window.globalCurrentHighlightStatus = isHighlighted; updateCrossBlockContextMenuOptions(isHighlighted, hasNote); // 使用 clientX/clientY(相对于视口)配合 position: fixed showContextMenu(event.clientX, event.clientY); } // ===== 修改:检查跨子块高亮状态 ===== function checkCrossBlockHighlight(affectedSubBlocks) { if (!window.data || !window.data.annotations) return false; const affectedSubBlockIds = affectedSubBlocks.map(sb => sb.subBlockId); // 查找跨子块标注 const crossBlockAnnotation = findCrossBlockAnnotation(affectedSubBlockIds, window.globalCurrentContentIdentifier); if (crossBlockAnnotation) { return true; } // 备用检查:是否所有子块都被独立高亮(兼容旧数据) for (const subBlock of affectedSubBlocks) { const hasHighlight = window.data.annotations.some(ann => ann.targetType === window.globalCurrentContentIdentifier && ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId === subBlock.subBlockId && (ann.motivation === 'highlighting' || ann.motivation === 'commenting') ); if (!hasHighlight) { return false; } } return true; } // ===== 修改:检查跨子块批注状态 ===== function checkCrossBlockNote(affectedSubBlocks) { if (!window.data || !window.data.annotations) return false; const affectedSubBlockIds = affectedSubBlocks.map(sb => sb.subBlockId); // 查找跨子块标注的批注 const crossBlockAnnotation = findCrossBlockAnnotation(affectedSubBlockIds, window.globalCurrentContentIdentifier); if (crossBlockAnnotation && crossBlockAnnotation.body && crossBlockAnnotation.body.length > 0 && crossBlockAnnotation.body[0].value && crossBlockAnnotation.body[0].value.trim() !== '') { return true; } // 备用检查:是否有任意子块有批注 for (const subBlock of affectedSubBlocks) { const hasNote = window.data.annotations.some(ann => ann.targetType === window.globalCurrentContentIdentifier && ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId === subBlock.subBlockId && ann.body && ann.body.length > 0 && ann.body[0].value && ann.body[0].value.trim() !== '' ); if (hasNote) { return true; } } return false; } // ===== 新增:更新跨子块上下文菜单 ===== function updateCrossBlockContextMenuOptions(isHighlighted, hasNote) { if (!annotationContextMenuElement) return; const highlightOption = annotationContextMenuElement.querySelector('[data-action="highlight-block"]'); const removeHighlightOption = document.getElementById('remove-highlight-option'); const addNoteOption = document.getElementById('add-note-option'); const editNoteOption = document.getElementById('edit-note-option'); const copyContentOption = document.getElementById('copy-content-option'); // 显示跨块高亮选项,并添加颜色子选项 if (highlightOption) { highlightOption.textContent = '高亮选中区域'; highlightOption.style.display = isHighlighted ? 'none' : 'block'; // 为跨子块高亮选项添加颜色子选项 if (!isHighlighted) { // 清除现有的颜色选项 const existingColorOptions = highlightOption.querySelectorAll('.color-option'); existingColorOptions.forEach(option => option.remove()); // 添加颜色选项 const colorOptions = [ { color: 'rgba(255, 255, 0, 0.3)', name: '黄色', value: 'yellow' }, { color: 'rgba(0, 255, 0, 0.3)', name: '绿色', value: 'green' }, { color: 'rgba(255, 192, 203, 0.3)', name: '粉色', value: 'pink' }, { color: 'rgba(135, 206, 235, 0.3)', name: '蓝色', value: 'blue' }, { color: 'rgba(255, 165, 0, 0.3)', name: '橙色', value: 'orange' } ]; const colorContainer = document.createElement('div'); colorContainer.className = 'color-submenu'; colorContainer.style.display = 'flex'; colorContainer.style.gap = '5px'; colorContainer.style.marginTop = '5px'; colorContainer.style.padding = '5px'; colorOptions.forEach(option => { const colorDiv = document.createElement('div'); colorDiv.className = 'color-option'; colorDiv.dataset.color = option.value; colorDiv.title = option.name; colorDiv.style.width = '20px'; colorDiv.style.height = '20px'; colorDiv.style.backgroundColor = option.color; colorDiv.style.border = '1px solid #ccc'; colorDiv.style.borderRadius = '3px'; colorDiv.style.cursor = 'pointer'; colorDiv.style.display = 'inline-block'; // 添加悬停效果 colorDiv.addEventListener('mouseenter', function() { colorDiv.style.transform = 'scale(1.1)'; colorDiv.style.borderColor = '#333'; }); colorDiv.addEventListener('mouseleave', function() { colorDiv.style.transform = 'scale(1)'; colorDiv.style.borderColor = '#ccc'; }); colorContainer.appendChild(colorDiv); }); highlightOption.appendChild(colorContainer); } } if (removeHighlightOption) { removeHighlightOption.textContent = '移除选中区域高亮'; removeHighlightOption.style.display = isHighlighted ? 'block' : 'none'; } if (copyContentOption) { copyContentOption.style.display = 'block'; } // 批注选项 if (isHighlighted) { if (addNoteOption) { addNoteOption.textContent = '为选中区域添加批注'; addNoteOption.style.display = hasNote ? 'none' : 'block'; } if (editNoteOption) { editNoteOption.textContent = '编辑选中区域批注'; editNoteOption.style.display = hasNote ? 'block' : 'none'; } } else { if (addNoteOption) addNoteOption.style.display = 'none'; if (editNoteOption) editNoteOption.style.display = 'none'; } } // ===== 重新设计:跨子块标注数据结构 ===== async function handleCrossBlockMenuAction(action, color, event) { const docId = getQueryParam('id'); if (!docId) { alert('错误:无法获取文档ID。'); hideContextMenu(); return; } const crossBlockAnnotationId = annotationContextMenuElement.dataset.contextCrossBlockAnnotationId; const affectedSubBlockIds = JSON.parse(annotationContextMenuElement.dataset.contextAffectedSubBlocks || '[]'); const selectedText = annotationContextMenuElement.dataset.contextSelectedText; const currentContentIdentifier = annotationContextMenuElement.dataset.contextContentIdentifier; console.log(`[跨子块操作] 执行操作: ${action}, 涉及 ${affectedSubBlockIds.length} 个子块`); try { if (action === 'highlight-block') { // 如果没有选择颜色,使用默认颜色 if (!color) { color = 'yellow'; // 默认黄色 console.log("跨子块高亮操作未选择颜色,使用默认颜色: " + color); } // 创建单一的跨子块标注对象 await createCrossBlockAnnotation(docId, affectedSubBlockIds, currentContentIdentifier, color, '', selectedText); console.log(`[跨子块高亮] 已创建跨子块标注,涉及 ${affectedSubBlockIds.length} 个子块`); } else if (action === 'remove-highlight') { // 移除跨子块标注 await removeCrossBlockAnnotation(affectedSubBlockIds, currentContentIdentifier); console.log(`[跨子块去高亮] 已移除跨子块标注`); } else if (action === 'add-note' || action === 'edit-note') { // 为跨子块标注添加/编辑批注 let noteText; if (action === 'edit-note') { const existingNote = findExistingCrossBlockNote(affectedSubBlockIds, currentContentIdentifier); noteText = prompt("编辑跨子块批注内容:", existingNote || ''); } else { noteText = prompt("为选中区域输入批注内容:", ""); } if (noteText === null) { // 用户取消 } else if (noteText.trim() === '') { alert('批注内容不能为空。'); } else { await addNoteToCrossBlockAnnotation(noteText, affectedSubBlockIds, currentContentIdentifier); console.log(`[跨子块批注] 已为选中区域添加批注`); } } else if (action === 'copy-content') { // 复制选中的跨子块内容 if (selectedText && selectedText.trim()) { navigator.clipboard.writeText(selectedText) .then(() => { console.log(`[跨子块复制] 已复制跨子块内容: ${selectedText.substring(0,50)}...`); }) .catch(err => { console.error('复制失败:', err); alert('复制内容失败。'); }); } else { alert('没有可复制的内容。'); } } // 刷新高亮显示 if (action !== 'copy-content') { const containerId = currentContentIdentifier + '-content-wrapper'; const container = document.getElementById(containerId); if (container && typeof window.applyBlockAnnotations === 'function') { window.applyBlockAnnotations(container, window.data.annotations, currentContentIdentifier); } } } catch (error) { console.error(`[跨子块操作] 操作 '${action}' 失败:`, error); alert(`跨子块操作失败: ${error.message}`); } finally { hideContextMenu(); } } // ===== 新增:创建跨子块标注 ===== async function createCrossBlockAnnotation(docId, affectedSubBlockIds, contentIdentifier, color, note = '', selectedText = '') { // 检查是否已存在跨子块标注 const existingAnnotation = findCrossBlockAnnotation(affectedSubBlockIds, contentIdentifier); if (existingAnnotation) { // 更新现有标注 existingAnnotation.highlightColor = color; existingAnnotation.modified = new Date().toISOString(); if (note) { existingAnnotation.body = [{ type: 'TextualBody', value: note, format: 'text/plain', purpose: 'commenting' }]; existingAnnotation.motivation = 'commenting'; } await updateAnnotationInDB(existingAnnotation); } else { // 创建新的跨子块标注 const rangeInfo = calculateCrossBlockRange(affectedSubBlockIds); const newAnnotation = { '@context': 'http://www.w3.org/ns/anno.jsonld', id: 'urn:uuid:' + _page_generateUUID(), type: 'Annotation', motivation: note ? 'commenting' : 'highlighting', created: new Date().toISOString(), docId: docId, targetType: contentIdentifier, highlightColor: color, isCrossBlock: true, // 标识这是跨子块标注 target: { source: docId, selector: [{ type: 'CrossBlockRangeSelector', startSubBlockId: rangeInfo.startSubBlockId, endSubBlockId: rangeInfo.endSubBlockId, startOffset: rangeInfo.startOffset, endOffset: rangeInfo.endOffset, affectedSubBlocks: affectedSubBlockIds, exact: selectedText || '' }] }, body: note ? [{ type: 'TextualBody', value: note, format: 'text/plain', purpose: 'commenting' }] : [] }; await saveAnnotationToDB(newAnnotation); if (!window.data.annotations) window.data.annotations = []; window.data.annotations.push(newAnnotation); } } // ===== 新增:计算跨子块范围信息 ===== function calculateCrossBlockRange(affectedSubBlockIds) { if (!affectedSubBlockIds.length) return null; // 优先使用跨子块检测时保存的原始 Range,避免上下文菜单点击导致选区变化 let range = (window.globalCurrentSelection && window.globalCurrentSelection.isCrossBlock && window.globalCurrentSelection.range) ? window.globalCurrentSelection.range.cloneRange() : null; if (!range) { const selection = window.getSelection(); if (!selection.rangeCount) return null; range = selection.getRangeAt(0); } // 找到起止子块元素(优先使用 crossBlockInfo 存下来的 DOM) let startSubBlock = (window.globalCurrentSelection && window.globalCurrentSelection.startSubBlock) || null; let endSubBlock = (window.globalCurrentSelection && window.globalCurrentSelection.endSubBlock) || null; if (!startSubBlock) { startSubBlock = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement.closest('.sub-block') : (range.startContainer.closest ? range.startContainer.closest('.sub-block') : null); } if (!endSubBlock) { endSubBlock = range.endContainer.nodeType === Node.TEXT_NODE ? range.endContainer.parentElement.closest('.sub-block') : (range.endContainer.closest ? range.endContainer.closest('.sub-block') : null); } const startSubBlockId = startSubBlock ? startSubBlock.dataset.subBlockId : affectedSubBlockIds[0]; const endSubBlockId = endSubBlock ? endSubBlock.dataset.subBlockId : affectedSubBlockIds[affectedSubBlockIds.length - 1]; // 计算相对各自子块文本的字符偏移 let startOffsetInSubBlock = 0; let endOffsetInSubBlock = 0; const fragmentTextLength = (frag) => { const walker = document.createTreeWalker(frag, NodeFilter.SHOW_TEXT, null); let len = 0; let n; const isInIndicator = (textNode) => { let p = textNode.parentNode; while (p) { if (p.nodeType === 1 && p.classList && p.classList.contains('cross-block-indicator')) return true; p = p.parentNode; } return false; }; while ((n = walker.nextNode())) { if (!isInIndicator(n)) len += (n.nodeValue ? n.nodeValue.length : 0); } return len; }; try { if (startSubBlock && startSubBlock.contains(range.startContainer)) { const r = document.createRange(); r.selectNodeContents(startSubBlock); r.setEnd(range.startContainer, range.startOffset); startOffsetInSubBlock = fragmentTextLength(r.cloneContents()); } else if (startSubBlock) { // 兜底:若浏览器把选区起点放到子块外,则认为偏移为0 startOffsetInSubBlock = 0; } if (endSubBlock && endSubBlock.contains(range.endContainer)) { const r2 = document.createRange(); r2.selectNodeContents(endSubBlock); r2.setEnd(range.endContainer, range.endOffset); endOffsetInSubBlock = fragmentTextLength(r2.cloneContents()); } else if (endSubBlock) { // 兜底:若浏览器把选区终点放到子块外,则认为到达末尾 const rr = document.createRange(); rr.selectNodeContents(endSubBlock); endOffsetInSubBlock = fragmentTextLength(rr.cloneContents()); } } catch (e) { console.warn('[跨子块] 计算偏移失败,使用回退 offset', e); startOffsetInSubBlock = range.startOffset || 0; endOffsetInSubBlock = range.endOffset || 0; } return { startSubBlockId: startSubBlockId, endSubBlockId: endSubBlockId, startOffset: startOffsetInSubBlock, endOffset: endOffsetInSubBlock, selectedText: (window.globalCurrentSelection && window.globalCurrentSelection.isCrossBlock && window.globalCurrentSelection.text) ? window.globalCurrentSelection.text : (window.getSelection ? window.getSelection().toString() : '') }; } // ===== 新增:查找跨子块标注 ===== function findCrossBlockAnnotation(affectedSubBlockIds, contentIdentifier) { if (!window.data || !window.data.annotations) return null; return window.data.annotations.find(ann => { if (ann.targetType !== contentIdentifier || !ann.isCrossBlock) return false; if (!ann.target || !ann.target.selector || !ann.target.selector[0]) return false; const selector = ann.target.selector[0]; if (!selector.affectedSubBlocks) return false; // 检查是否包含相同的子块ID集合 const annotationSubBlocks = selector.affectedSubBlocks.sort(); const targetSubBlocks = affectedSubBlockIds.sort(); return JSON.stringify(annotationSubBlocks) === JSON.stringify(targetSubBlocks); }); } // ===== 新增:移除跨子块标注 ===== async function removeCrossBlockAnnotation(affectedSubBlockIds, contentIdentifier) { const annotation = findCrossBlockAnnotation(affectedSubBlockIds, contentIdentifier); if (annotation) { await deleteAnnotationFromDB(annotation.id); const index = window.data.annotations.findIndex(ann => ann.id === annotation.id); if (index > -1) { window.data.annotations.splice(index, 1); } } } // ===== 新增:为跨子块标注添加批注 ===== async function addNoteToCrossBlockAnnotation(noteText, affectedSubBlockIds, contentIdentifier) { const annotation = findCrossBlockAnnotation(affectedSubBlockIds, contentIdentifier); if (annotation) { annotation.body = [{ type: 'TextualBody', value: noteText, format: 'text/plain', purpose: 'commenting' }]; annotation.modified = new Date().toISOString(); annotation.motivation = 'commenting'; await updateAnnotationInDB(annotation); } } // ===== 新增:创建或更新子块标注 ===== async function createOrUpdateSubBlockAnnotation(docId, subBlockId, contentIdentifier, color, note = '', groupId = null) { // 查找现有标注 const existingAnnotation = window.data.annotations.find(ann => ann.targetType === contentIdentifier && ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId === subBlockId ); if (existingAnnotation) { // 更新现有标注 existingAnnotation.highlightColor = color; existingAnnotation.modified = new Date().toISOString(); if (groupId) existingAnnotation.groupId = groupId; if (note) { existingAnnotation.body = [{ type: 'TextualBody', value: note, format: 'text/plain', purpose: 'commenting' }]; existingAnnotation.motivation = 'commenting'; } else if (!existingAnnotation.body || existingAnnotation.body.length === 0) { existingAnnotation.motivation = 'highlighting'; } await updateAnnotationInDB(existingAnnotation); } else { // 创建新标注 const newAnnotation = { '@context': 'http://www.w3.org/ns/anno.jsonld', id: 'urn:uuid:' + _page_generateUUID(), type: 'Annotation', motivation: note ? 'commenting' : 'highlighting', created: new Date().toISOString(), docId: docId, targetType: contentIdentifier, highlightColor: color, target: { source: docId, selector: [{ type: 'SubBlockSelector', subBlockId: subBlockId }] }, body: note ? [{ type: 'TextualBody', value: note, format: 'text/plain', purpose: 'commenting' }] : [] }; if (groupId) newAnnotation.groupId = groupId; // 设置 exact 字段 const subBlockElement = document.querySelector(`[data-sub-block-id="${subBlockId}"]`); if (subBlockElement) { newAnnotation.target.selector[0].exact = subBlockElement.textContent.trim(); } await saveAnnotationToDB(newAnnotation); if (!window.data.annotations) window.data.annotations = []; window.data.annotations.push(newAnnotation); } } // ===== 修改:查找跨子块批注 ===== function findExistingCrossBlockNote(affectedSubBlockIds, contentIdentifier) { if (!window.data || !window.data.annotations) return ''; // 首先查找跨子块标注 const crossBlockAnnotation = findCrossBlockAnnotation(affectedSubBlockIds, contentIdentifier); if (crossBlockAnnotation && crossBlockAnnotation.body && crossBlockAnnotation.body.length > 0 && crossBlockAnnotation.body[0].value) { return crossBlockAnnotation.body[0].value; } // 备用:查找第一个子块的批注 for (const subBlockId of affectedSubBlockIds) { const annotation = window.data.annotations.find(ann => ann.targetType === contentIdentifier && ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId === subBlockId && ann.body && ann.body.length > 0 && ann.body[0].value ); if (annotation) { return annotation.body[0].value; } } return ''; } // 暴露新功能 window.detectCrossBlockSelection = detectCrossBlockSelection; window.handleCrossBlockAnnotation = handleCrossBlockAnnotation; // 保留旧函数以保持向后兼容性 window.checkIfTargetIsHighlighted = checkIfTargetIsHighlighted; window.checkIfTargetHasNote = checkIfTargetHasNote; // 保留旧函数以保持向后兼容性 window.updateContextMenuOptions = updateContextMenuOptions; window.showContextMenu = showContextMenu; window.initializeGlobalAnnotationVariables = function() { window.globalCurrentSelection = null; // window.globalCurrentTargetElement = null; // 重要性降低 window.globalCurrentHighlightStatus = false; window.globalCurrentContentIdentifier = ''; // 仍然初始化,但应减少直接依赖 };