// ======================================== // 模块:侧边栏备注功能 // ======================================== import { $, debounce, throttle } from './utils.js'; import { config } from './config.js'; let isEnabled = false, observers = {}, editorNode = null, dragTimeout = null, dragMutationObserver = null, connectionCleanup = null; // 配置常量 const CONFIG = { SIDEBAR_WIDTH: 230, MARGIN: 10, EDITING_MARGIN: 5, MIN_INPUT_HEIGHT: 60, CONNECTION_OPACITY: 0.5, Z_INDEX_EDITING: 999 }; const autoResizeDiv = div => { div.style.height = 'auto'; div.style.height = (div.scrollHeight + 1) + 'px'; }; // 确保光标正确定位到最后一个字符后 const setCursorPositionToEnd = el => { const range = document.createRange(); const selection = window.getSelection(); // 清除现有选择 selection.removeAllRanges(); // 将光标定位到内容末尾 if (el.lastChild) { range.setStartAfter(el.lastChild); } else { range.selectNodeContents(el); range.collapse(false); } selection.addRange(range); el.focus(); }; // 获取块节点 const getBlockNode = el => { while (el && !el.dataset.nodeId) el = el.parentElement; return el && el.closest('[data-type="NodeBlockQueryEmbed"]')?.dataset.nodeId ? el.closest('[data-type="NodeBlockQueryEmbed"]') : el; }; // 检查块类型是否有效(用于行内备注) const isValidBlockTypeForInlineMemo = block => { return block && ['NodeParagraph', 'NodeHeading'].includes(block.getAttribute('data-type')); }; // 检查是否在嵌入块内 const isInsideEmbedBlock = blockEl => { return blockEl?.closest('[data-type="NodeBlockQueryEmbed"]'); }; // 获取备注内容的HTML格式 const getMemoContentHtml = memoItem => { return memoItem.querySelector('.memo-content-view')?.innerHTML.replace(/
/g, '\n') || ''; }; // 更新块元素的备注内容 const updateBlockMemoContent = (blockEl, oldContent, newContent) => { const memoSpans = Array.from(blockEl.querySelectorAll('span[data-type*="inline-memo"]')); memoSpans.forEach(span => { if (span.getAttribute('data-inline-memo-content') === oldContent) { span.setAttribute('data-inline-memo-content', newContent); } }); // 更新块内容 return updateBlock(blockEl); }; // 删除块元素中的备注 const removeBlockMemoContent = (blockEl, memoContent) => { const memoSpans = Array.from(blockEl.querySelectorAll('span[data-type*="inline-memo"]')); memoSpans.forEach(span => { const content = span.getAttribute('data-inline-memo-content'); // 只删除内容匹配的备注元素 if (content === memoContent) { let types = (span.getAttribute("data-type") || "").split(" ").filter(t => t !== "inline-memo"); if (types.length) { span.setAttribute("data-type", types.join(" ")); span.removeAttribute("data-inline-memo-content"); } else { span.outerHTML = span.innerHTML; } } }); // 更新块内容 return updateBlock(blockEl); }; // 统一的块更新函数 const updateBlock = (blockEl) => { return fetch('/api/block/updateBlock', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Token ${window.siyuan?.config?.api?.token ?? ''}` }, body: JSON.stringify({ dataType: 'html', data: blockEl.outerHTML, id: blockEl.dataset.nodeId }) }); }; // 高亮备注元素 const highlightMemoElements = (blockEl, memoContent) => { const memoSpans = Array.from(blockEl.querySelectorAll('span[data-type*="inline-memo"]')); // 高亮所有相同内容的备注元素 const targetSpans = memoSpans.filter(span => span.getAttribute('data-inline-memo-content') === memoContent ); targetSpans.forEach(span => span.classList.add('memo-span-highlight')); // 返回最后一个匹配的备注元素 return targetSpans[targetSpans.length - 1]; }; // 创建备忘录连接线 const createMemoConnection = (memoDiv, memoSpan) => { removeMemoConnection(); if (!memoDiv || !memoSpan) return; const container = document.createElement('div'); container.id = 'memo-connection-container'; container.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:998;will-change:transform;'; container.innerHTML = ` `; document.body.appendChild(container); const path = container.querySelector('path'); const updatePath = () => { const a = memoDiv.getBoundingClientRect(), b = memoSpan.getBoundingClientRect(); if (!a.width || !b.width) return; const sx = b.right, sy = b.top + b.height / 2, ex = a.left - 6, ey = a.top + a.height / 2; const o = Math.min(Math.abs(ex - sx) * 0.5, 200); path.setAttribute('d', `M${sx} ${sy}C${sx + o} ${sy},${ex - o} ${ey},${ex} ${ey}`); }; updatePath(); const onScroll = () => requestAnimationFrame(updatePath); window.addEventListener('scroll', onScroll, true); window.addEventListener('resize', onScroll); connectionCleanup = () => { window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onScroll); container.remove(); connectionCleanup = null; }; }; const removeMemoConnection = () => connectionCleanup?.(); // 判断某块及其所有父块是否有折叠 const isAnyAncestorFolded = block => { for (let current = block; current; current = current.parentElement) { if (current.getAttribute?.('fold') === '1') return true; } return false; }; // 拖拽监听 const observeDragTitle = () => { if (dragMutationObserver) return; const setupObserver = () => { const dragEl = document.getElementById('drag'); if (!dragEl) { dragTimeout = setTimeout(setupObserver, 1000); return; } dragMutationObserver = new MutationObserver(refreshEditor); dragMutationObserver.observe(dragEl, { attributes: true, attributeFilter: ['title'] }); }; setupObserver(); }; const unobserveDragTitle = () => { dragMutationObserver?.disconnect(); dragMutationObserver = null; dragTimeout && clearTimeout(dragTimeout); dragTimeout = null; }; // 刷新侧边栏备注位置 const refreshMemoOffset = (main, sidebar) => { requestAnimationFrame(() => { let lastBottom = 0; Array.from(sidebar.querySelectorAll('.memo-item')) .filter(item => item.style.display !== 'none') .forEach(memoItem => { const nodeId = memoItem.getAttribute('data-node-id'); const memoType = memoItem.getAttribute('data-memo-type'); const block = main.querySelector(`div[data-node-id="${nodeId}"]`); if (!block) return; // 行内备注只处理 NodeParagraph 和 NodeHeading 类型的节点 const isValidBlockType = memoType === 'block' || isValidBlockTypeForInlineMemo(block); if (!isValidBlockType) return; let targetTop = 0; if (memoType === 'block') { // 块备注:与块的顶部对齐 const blockRect = block.getBoundingClientRect(); const mainRect = main.getBoundingClientRect(); targetTop = blockRect.top - mainRect.top - (memoItem.offsetHeight / 2); } else { // 行内备注:定位到第一个相关元素的位置 const blockRect = block.getBoundingClientRect(); const mainRect = main.getBoundingClientRect(); // 获取所有行内备注元素 const memoSpans = Array.from(block.querySelectorAll('span[data-type*="inline-memo"]')); // 使用innerHTML而不是textContent来正确处理换行符 const memoContent = getMemoContentHtml(memoItem); // 找到第一个包含相同备注内容的元素 const targetSpan = memoSpans.find(span => span.getAttribute('data-inline-memo-content') === memoContent ); targetTop = targetSpan ? targetSpan.getBoundingClientRect().top - mainRect.top + (targetSpan.getBoundingClientRect().height / 2) - (memoItem.offsetHeight / 2) : blockRect.top - mainRect.top + blockRect.height / 2 - memoItem.offsetHeight / 2; } // 重叠检查 - 更智能的间距处理 const isEditing = memoItem.classList.contains('editing'); // 对于编辑中的项,使用更精确的位置计算 if (isEditing) { // 确保编辑项不会与前一项重叠,但也不要过度拉开距离 targetTop = Math.max(targetTop, lastBottom + CONFIG.EDITING_MARGIN); // 编辑时使用较小的间距 } else { // 对于非编辑项,保持正常的间距 targetTop = Math.max(targetTop, lastBottom + CONFIG.MARGIN); } memoItem.style.position = 'absolute'; memoItem.style.top = `${targetTop}px`; memoItem.style.transition = 'top 0.3s cubic-bezier(0.4,0,0.2,1),transform 0.3s cubic-bezier(0.4,0,0.2,1)'; lastBottom = targetTop + memoItem.offsetHeight; }); }); }; // 添加侧边栏 const addSideBar = main => { const sidebar = main.parentElement.querySelector('#protyle-sidebar'); const title = main.parentElement.querySelector('div.protyle-title'); if (!sidebar && title) { const newSidebar = document.createElement('div'); newSidebar.id = 'protyle-sidebar'; title.insertAdjacentElement('beforeend', newSidebar); newSidebar.style.cssText = `position:absolute;right:-${CONFIG.SIDEBAR_WIDTH}px;width:${CONFIG.SIDEBAR_WIDTH}px;`; main.style.minWidth = '90%'; return newSidebar; } return sidebar; }; // 处理备注编辑和保存 const handleMemoEdit = (memoDiv, el, main, sidebar) => { const memoType = memoDiv.getAttribute('data-memo-type'); const isBlockMemo = memoType === 'block'; // 获取旧值 const old = isBlockMemo ? (el.getAttribute('memo') || '') : (memoDiv.querySelector('.memo-content-view')?.innerHTML.replace(/
/g, '\n') || ''); const input = document.createElement('div'); input.className = 'memo-edit-input'; input.contentEditable = 'true'; input.innerHTML = old.replace(/\n/g, '
'); input.setAttribute('placeholder', '输入备注内容...'); input.style.cssText = `width:100%;min-height:${CONFIG.MIN_INPUT_HEIGHT}px;padding:6px;border:1px solid var(--b3-theme-primary);border-radius:8px;font-size:0.8em;resize:vertical;box-sizing:border-box;overflow:auto;outline:none;white-space:pre-wrap;word-break:break-all;overflow-y:hidden;`; input.addEventListener('input', () => autoResizeDiv(input)); const save = () => { // 保留原始HTML格式用于显示,同时提取纯文本用于比较 const val = input.innerHTML.replace(/
/gi, '\n').replace(/<\/div>/gi, '').replace(//gi, '\n').replace(/

/gi, '\n').replace(/<\/p>/gi, '').replace(/ /g, ' ').replace(/\n\s*\n\s*\n+/g, '\n\n').trim(); const htmlVal = input.innerHTML; if (val !== old) { if (isBlockMemo) { // 块备注更新逻辑 el.setAttribute('memo', val); updateBlock(el); } else { // 行内备注更新逻辑 const nodeId = memoDiv.getAttribute('data-node-id'); const blockEl = main.querySelector(`div[data-node-id="${nodeId}"]`); if (blockEl) { // 更新块备注内容 updateBlockMemoContent(blockEl, old, val); } } } const newDiv = document.createElement('div'); newDiv.className = 'memo-content-view'; newDiv.style.cssText = 'font-size:0.8em;margin-bottom:4px;cursor:pointer;'; // 显示值设置 - 正确处理换行符 const displayVal = htmlVal || '点击编辑备注...'; newDiv.innerHTML = displayVal; newDiv.onclick = (e) => { // 检查是否已经在编辑状态 if (memoDiv.classList.contains('editing')) return; const { input: newInput, save: newSave } = handleMemoEdit(memoDiv, el, main, sidebar); newDiv.replaceWith(newInput); memoDiv.classList.add('editing'); memoDiv.style.zIndex = `${CONFIG.Z_INDEX_EDITING}`; newInput.focus(); setCursorPositionToEnd(newInput); requestAnimationFrame(() => { autoResizeDiv(newInput); refreshMemoOffset(main, sidebar); }); }; input.replaceWith(newDiv); memoDiv.classList.remove('editing'); memoDiv.style.zIndex = ''; refreshMemoOffset(main, sidebar); }; // 设置输入框事件 input.onblur = () => setTimeout(() => { if (document.activeElement !== input) save(); }, 100); input.onkeydown = e => { if ((e.key === 'Enter' && e.ctrlKey) || e.key === 'Escape') { e.preventDefault(); save(); } else if (e.key === 'a' && e.ctrlKey) { e.preventDefault(); const range = document.createRange(); range.selectNodeContents(input); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } }; input.addEventListener('paste', e => { e.preventDefault(); const text = e.clipboardData?.getData('text'); text && document.execCommand('insertText', false, text); }, true); autoResizeDiv(input); return { input, save }; }; // 刷新侧边栏备注 const refreshSideBarMemos = (main, sidebar) => { // 清除之前的内容 sidebar.innerHTML = ''; const frag = document.createDocumentFragment(); const isReadonly = main.getAttribute?.('data-readonly') === 'true'; let visibleMemoCount = 0; // 使用Set来避免重复的块ID const processedBlocks = new Set(); // 提前检查当前启用的模式 const isBlockMemoMode = document.documentElement.hasAttribute('savor-sidebar-block-memo'); // 获取所有块 const allBlocks = Array.from(main.querySelectorAll('[data-node-id]')); // 创建块顺序映射 const blockOrderMap = new Map(); allBlocks.forEach((block, index) => { blockOrderMap.set(block.dataset.nodeId, index); }); const allMemos = []; allBlocks.forEach(block => { const blockId = block.dataset.nodeId; // 避免重复处理 if (processedBlocks.has(blockId)) return; processedBlocks.add(blockId); // 添加块备注 if (block.hasAttribute('memo')) { allMemos.push({ element: block, type: 'block', content: block.getAttribute('memo') || '', text: ' ', blockId: blockId, elements: [], index: -1, blockOrder: blockOrderMap.get(blockId) }); } // 收集行内备注 if (!isBlockMemoMode) { // 行内备注只处理 NodeParagraph 和 NodeHeading 类型的节点 const isValidBlockType = isValidBlockTypeForInlineMemo(block); if (isValidBlockType) { // 收集块内的行内备注 const inlineMemos = block.querySelectorAll('span[data-type*="inline-memo"]'); const memoGroups = []; for (let i = 0; i < inlineMemos.length; i++) { const el = inlineMemos[i]; const memoContent = el.getAttribute('data-inline-memo-content') || ''; const memoText = el.textContent || ''; // 检查是否与前一个元素相邻且内容相同 let isAdjacentAndSame = false; if (memoGroups.length > 0) { const lastGroup = memoGroups[memoGroups.length - 1]; const lastElement = lastGroup.elements[lastGroup.elements.length - 1].element; // 检查是否相邻 let nextSibling = lastElement.nextSibling; while (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && nextSibling.textContent.trim() === '') { nextSibling = nextSibling.nextSibling; } if (nextSibling === el && lastGroup.content === memoContent) { isAdjacentAndSame = true; } } if (isAdjacentAndSame) { // 添加到当前组 const currentGroup = memoGroups[memoGroups.length - 1]; currentGroup.elements.push({ element: el, index: i }); // 避免重复文本 if (!currentGroup.texts.includes(memoText)) { currentGroup.texts.push(memoText); } } else { // 创建新组 memoGroups.push({ elements: [{ element: el, index: i }], texts: [memoText], content: memoContent }); } } // 处理每个分组 memoGroups.forEach(group => { const memoBlock = getBlockNode(group.elements[0].element); const memoBlockId = memoBlock?.dataset.nodeId || blockId; const combinedText = group.texts.join(''); allMemos.push({ elements: group.elements, element: group.elements[0].element, type: 'inline', content: group.content, text: combinedText, blockId: blockId, blockOrder: blockOrderMap.get(memoBlockId) }); }); } } }); // 按块顺序排序 allMemos.sort((a, b) => a.blockOrder - b.blockOrder); // 如果没有备注,直接返回 if (allMemos.length === 0) { sidebar.removeAttribute('data-memo-count'); // 修复:即使没有备注也要确保清除 Sv-memo 类 const protyleContent = main.closest('.protyle')?.querySelector('.protyle-content'); if (protyleContent) { protyleContent.classList.remove('Sv-memo'); } return; } // 计算块折叠状态 const blockFoldStates = new Map(); allMemos.forEach(memoData => { if (!blockFoldStates.has(memoData.blockId)) { const block = main.querySelector(`div[data-node-id="${memoData.blockId}"]`); blockFoldStates.set(memoData.blockId, block && isAnyAncestorFolded(block)); } }); // 显示备注 allMemos.forEach(memoData => { const block = main.querySelector(`div[data-node-id="${memoData.blockId}"]`); // 如果块不存在,跳过 if (!block) return; const isBlockFolded = blockFoldStates.get(memoData.blockId) || false; const { content: memo, text } = memoData; const memoDiv = document.createElement('div'); memoDiv.className = 'memo-item'; memoDiv.setAttribute('data-node-id', memoData.blockId); memoDiv.setAttribute('data-memo-index', memoData.type === 'inline' ? (memoData.elements?.[0]?.index || -1) : -1); memoDiv.setAttribute('data-memo-type', memoData.type); // 存储元素数量 memoData.type === 'inline' && memoDiv.setAttribute('data-memo-element-count', memoData.elements?.length || 1); memoDiv.style.cssText = 'margin:0px 0px 8px 16px;padding:8px;border-radius:8px;position:relative;width:220px;box-shadow:rgba(0, 0, 0, 0.03) 0px 12px 20px, var(--b3-border-color) 0px 0px 0px 1px inset;'; // 显示/隐藏逻辑 memoDiv.style.display = isBlockFolded ? 'none' : ''; !isBlockFolded && visibleMemoCount++; const formattedDisplayText = text; const memoContentStyle = isReadonly ? 'cursor:auto;' : 'cursor:pointer;'; memoDiv.innerHTML = `

${formattedDisplayText}
${memo ? memo.replace(/\n/g, '
') : '点击编辑备注...'}
`; const titleDiv = memoDiv.querySelector('.memo-title-with-dot'); // 创建删除按钮 if (titleDiv && !isReadonly) { // 检查是否在嵌入块内 const blockEl = main.querySelector(`div[data-node-id="${memoData.blockId}"]`); const isInsideEmbedBlockResult = isInsideEmbedBlock(blockEl); // 如果不在嵌入块内,才显示删除按钮 if (!isInsideEmbedBlockResult) { const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = ``; deleteBtn.style.cssText = 'position:absolute;top:6px;right:6px;padding:0;border:none;border-radius:6px;cursor:pointer;z-index:2;'; deleteBtn.setAttribute('data-action', 'delete'); titleDiv.appendChild(deleteBtn); } } const memoContentDiv = memoDiv.querySelector('.memo-content-view'); if (!isReadonly) { memoContentDiv.onclick = (e) => { e.stopPropagation(); if (memoDiv.classList.contains('editing')) return; const nodeId = memoData.blockId; const blockEl = main.querySelector(`div[data-node-id="${nodeId}"]`); // 检查是否在嵌入块内 const isInsideEmbedBlockResult = isInsideEmbedBlock(blockEl); // 如果在嵌入块内,禁止编辑 if (isInsideEmbedBlockResult) return; if (blockEl) { // 行内备注只处理 NodeParagraph 和 NodeHeading 类型的节点 const isValidBlockType = memoData.type === 'block' || isValidBlockTypeForInlineMemo(blockEl); if (isValidBlockType) { // 获取目标元素 let targetElement = memoData.element; if (memoData.type === 'inline' && memoData.elements?.length > 0) { targetElement = memoData.elements[0].element; } const { input, save } = handleMemoEdit(memoDiv, targetElement, main, sidebar); memoContentDiv.replaceWith(input); memoDiv.classList.add('editing'); memoDiv.style.zIndex = `${CONFIG.Z_INDEX_EDITING}`; input.focus(); setCursorPositionToEnd(input); requestAnimationFrame(() => { autoResizeDiv(input); refreshMemoOffset(main, sidebar); }); } } }; } frag.appendChild(memoDiv); }); sidebar.setAttribute('data-memo-count', `共 ${visibleMemoCount} 个备注`); sidebar.appendChild(frag); // 事件委托 if (!sidebar._delegated) { const handleMouseInteraction = (e, isEnter) => { const item = e.target.closest('.memo-item'); if (!item || !sidebar.contains(item)) return; const rt = e.relatedTarget; if (rt && item.contains(rt)) return; if (isEnter) { const nodeId = item.getAttribute('data-node-id'); const memoType = item.getAttribute('data-memo-type'); const blockEl = main.querySelector(`div[data-node-id="${nodeId}"]`); // 检查是否在嵌入块内 const isInsideEmbedBlock = blockEl?.closest('[data-type="NodeBlockQueryEmbed"]'); if (blockEl) { // 行内备注只处理 NodeParagraph 和 NodeHeading 类型的节点 const isValidBlockType = memoType === 'block' || isValidBlockTypeForInlineMemo(blockEl); if (!isValidBlockType) return; if (memoType === 'block') { blockEl.classList.add('memo-span-highlight'); createMemoConnection(item, blockEl); } else { // 高亮备注元素 const memoContent = getMemoContentHtml(item); const targetSpan = highlightMemoElements(blockEl, memoContent); targetSpan && createMemoConnection(item, targetSpan); } } } else { removeMemoConnection(); main.querySelectorAll('.memo-span-highlight').forEach(el => el.classList.remove('memo-span-highlight')); } }; // 鼠标事件处理 ['mouseover', 'mouseout'].forEach(eventType => { sidebar.addEventListener(eventType, e => handleMouseInteraction(e, eventType === 'mouseover')); }); // 点击事件处理 sidebar.addEventListener('click', e => { const btn = e.target.closest('button[data-action="delete"]'); if (!btn) return; const item = btn.closest('.memo-item'); if (!item) return; e.stopPropagation(); const nodeId = item.getAttribute('data-node-id'); const memoType = item.getAttribute('data-memo-type'); const blockEl = main.querySelector(`div[data-node-id="${nodeId}"]`); // 检查是否在嵌入块内 const isInsideEmbedBlockResult = isInsideEmbedBlock(blockEl); // 如果在嵌入块内,禁止删除 if (isInsideEmbedBlockResult) return; item.remove(); removeMemoConnection(); // 确保connectionCleanup被重置 connectionCleanup = null; if (blockEl) { // 行内备注只处理 NodeParagraph 和 NodeHeading 类型的节点 const isValidBlockType = memoType === 'block' || isValidBlockTypeForInlineMemo(blockEl); if (isValidBlockType) { if (memoType === 'block') { blockEl.removeAttribute('memo'); } else { // 删除当前合并项中包含的行内备注 const memoContent = getMemoContentHtml(item); removeBlockMemoContent(blockEl, memoContent); } updateBlock(blockEl); } } }); sidebar._delegated = true; } refreshMemoOffset(main, sidebar); const protyleContent = main.closest('.protyle')?.querySelector('.protyle-content'); // 修复:无论是否有可见备注,都应该正确设置 Sv-memo 类 if (protyleContent) { if (visibleMemoCount > 0) { protyleContent.classList.add('Sv-memo'); } else { protyleContent.classList.remove('Sv-memo'); } } }; // 更新内联备注到思源 const updateInlineMemo = async (el, content) => { try { const blockEl = getBlockNode(el); if (!blockEl?.dataset.nodeId) return; el.setAttribute('data-inline-memo-content', content); await fetch('/api/block/updateBlock', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Token ${window.siyuan?.config?.api?.token ?? ''}` }, body: JSON.stringify({ dataType: 'html', data: blockEl.outerHTML, id: blockEl.dataset.nodeId }) }); } catch (e) { // 更新备注出错: e } }; // 刷新编辑器 const refreshEditor = () => { if (!editorNode) return; Object.values(observers).flat().forEach(o => o.disconnect()); observers = {}; editorNode.querySelectorAll('div.protyle-wysiwyg').forEach(main => { let sidebar = main.parentElement.querySelector('#protyle-sidebar'); if (!sidebar && isEnabled) sidebar = addSideBar(main); if (!sidebar) return; const mainId = main.parentElement.parentElement.getAttribute('data-id'); let refreshTimer = null; const observer = new MutationObserver(() => { clearTimeout(refreshTimer); refreshTimer = setTimeout(() => { if (isEnabled) refreshSideBarMemos(main, sidebar); refreshMemoOffset(main, sidebar); }, 100); }); observer.observe(main, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-inline-memo-content', 'data-readonly', 'fold', 'memo'] }); observers[mainId] = [observer]; refreshSideBarMemos(main, sidebar); const protyleContent = main.closest('.protyle')?.querySelector('.protyle-content'); if (protyleContent?._sidebarMemoScrollBinded === undefined) { let scheduled = false; const onScroll = () => { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { refreshMemoOffset(main, sidebar); scheduled = false; }); }; protyleContent.addEventListener('scroll', onScroll, { passive: true }); protyleContent._sidebarMemoScrollBinded = true; } }); // 修复:在全局范围内检查并清除没有备注的文档的 Sv-memo 类 document.querySelectorAll('.protyle-content').forEach(pc => { const main = pc.closest('.protyle')?.querySelector('.protyle-wysiwyg'); // 增强检查逻辑:不仅检查是否有备注元素,还要检查侧边栏是否为空 if (main) { const sidebar = main.parentElement.querySelector('#protyle-sidebar'); // 如果侧边栏存在但为空,或者完全没有备注元素,则移除 Sv-memo 类 if ((sidebar && (!sidebar.querySelector('.memo-item') || sidebar.children.length === 0)) || (!main.querySelector('span[data-type*="inline-memo"]') && !main.querySelector('[memo]'))) { pc.classList.remove('Sv-memo'); } } }); }; // 开关侧边栏 const openSideBar = (open, save = false) => { if (isEnabled === open) return; if (!editorNode && open) { const wait = () => { editorNode = document.querySelector('div.layout__center'); editorNode ? openSideBar(open, save) : setTimeout(wait, 100); }; wait(); return; } isEnabled = open; if (open) { window.siyuan?.eventBus?.on('loaded-protyle', refreshEditor); refreshEditor(); observeDragTitle(); } else { window.siyuan?.eventBus?.off('loaded-protyle', refreshEditor); Object.values(observers).flat().forEach(o => o.disconnect()); observers = {}; editorNode?.querySelectorAll('div.protyle-wysiwyg').forEach(main => { main.parentElement.querySelector('#protyle-sidebar')?.remove(); const protyleContent = main.closest('.protyle')?.querySelector('.protyle-content'); protyleContent?.classList.remove('Sv-memo'); }); unobserveDragTitle(); } save && config.set('sidebarMemoEnabled', open ? '1' : '0'); }; const init = () => { editorNode = document.querySelector('div.layout__center'); const shouldEnable = document.documentElement.hasAttribute('savor-sidebar-memo') || document.documentElement.hasAttribute('savor-sidebar-block-memo'); openSideBar(shouldEnable, true); }; // 清理函数 const cleanupSidebarMemo = () => { try { openSideBar(false); } catch (e) { // 关闭侧边栏失败: e } dragMutationObserver?.disconnect(); dragMutationObserver = null; dragTimeout && clearTimeout(dragTimeout); dragTimeout = null; removeMemoConnection(); isEnabled = false; observers = {}; editorNode = null; connectionCleanup = null; document.getElementById('sb-resizer-styles')?.remove(); document.querySelectorAll('.protyle-content.Sv-memo').forEach(el => { el.classList.remove('Sv-memo'); }); }; // 初始化侧边栏备注模块 export const initSidebarMemoModule = () => { window.sidebarMemo = { init, openSideBar, isEnabled: () => isEnabled, setEnabled: enabled => openSideBar(enabled, true), unobserveDragTitle }; window.cleanupSidebarMemo = cleanupSidebarMemo; init(); }; export { cleanupSidebarMemo };