Files
server-configs/siyuan/temp/bazaar/package/01ff2z8/js/modules/sidebarMemo.js
2026-02-13 22:24:27 +08:00

899 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========================================
// 模块:侧边栏备注功能
// ========================================
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(/<br>/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 = `
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:${CONFIG.CONNECTION_OPACITY};">
<path stroke="var(--Sv-dock-item--activefocus-background)" stroke-width="2" fill="none" stroke-dasharray="6,4" stroke-linecap="round">
<animate attributeName="stroke-dashoffset" from="20" to="-20" dur="1.5s" repeatCount="indefinite"/>
</path>
</svg>`;
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(/<br>/g, '\n') || '');
const input = document.createElement('div');
input.className = 'memo-edit-input';
input.contentEditable = 'true';
input.innerHTML = old.replace(/\n/g, '<br>');
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(/<div>/gi, '\n').replace(/<\/div>/gi, '').replace(/<br\s*\/?>/gi, '\n').replace(/<p>/gi, '\n').replace(/<\/p>/gi, '').replace(/&nbsp;/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 || '<span style="color:#bbb;">点击编辑备注...</span>';
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: '<svg width="14" height="14"><use xlink:href="#iconM"></use></svg> ',
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 = `<div class="memo-title-with-dot" style="font-weight:bold;margin-bottom:4px;font-size:0.9em;display:flex;"><span class="memo-title-dot"></span>${formattedDisplayText}</div><div class="memo-content-view" style="${memoContentStyle}font-size:0.8em;margin-bottom:4px;">${memo ? memo.replace(/\n/g, '<br>') : '<span style="color:#bbb;">点击编辑备注...</span>'}</div>`;
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 = `<svg class="b3-menu__icon" style="vertical-align:middle;"><use xlink:href="#iconTrashcan"></use></svg>`;
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 };