导航目录大体功能:(使用方法:把代码放进 js 代码片段,粘贴完注意第 8 条)
- 随着笔记标签页的切换,导航内容也能相应切换
- 目录字体大小可调
- 可折叠起来
- 导航目录过长,可滚动
- 鼠标点击目录,可以导航到正文相应位置
- 鼠标悬停在目录上,可弹出浮窗
- 鼠标单击块,浮动大纲中对应的标题内容颜色会改变。再次点击会找上一级标题
看下图,应该能明白。更新时间 20250227,修复了一些 bug
// ========== 工具函数 ========== // 使用原生DOMParser解析HTML并提取纯文本内容 const parseHtmlToText = (html) => { return html.replace(/<[^>]+>/g, '').replace(/ /g, ' '); // 注意,使用时要在 后面加一个分号; }; // 通用高亮函数,返回是否匹配成功 function highlightOutlineElements(outlineContent, nodeId, textContent) { const outlineElements = outlineContent.querySelectorAll("[data-href]"); let isMatched = false; // 用于标记是否找到匹配项 outlineElements.forEach((element) => { const href = element.getAttribute('data-href'); const hrefId = href.split('/').pop(); const isMatch = nodeId ? hrefId === nodeId : element.textContent.trim() === textContent; // 只在必要时修改样式 if (isMatch && element.style.backgroundColor !== 'green') { element.style.backgroundColor = 'green'; // 高亮背景颜色 isMatched = true; // 标记匹配成功 } else if (!isMatch && element.style.backgroundColor === 'green') { element.style.backgroundColor = ''; // 恢复默认背景颜色 } }); return isMatched; // 返回是否匹配成功 } // ========== 数据获取函数 ========== // 获取文档信息,包括 rootID async function getRootID({ id }) { const response = await fetch(`/api/block/getDocInfo`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id }), }); const data = await response.json(); return data.data.rootID; // 直接返回 rootID } // 获取文档大纲的函数 const getDocOutline = async (docId) => { try { const response = await fetch(`/api/outline/getDocOutline`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: docId }), }); if (!response.ok) { throw new Error(`网络请求失败:${response.status}`); } const data = await response.json(); if (data.code === 0) { return data.data; } else { console.error('获取文档目录结构失败:', data.msg); return null; } } catch (error) { console.error('获取文档目录结构失败:', error.message); return null; } }; // ========== 大纲处理函数 ========== // 收集大纲标题的函数 const collectTitles = (data) => { let titles = []; for (let item of data) { let text = (item.name || item.content).trim(); // 解析HTML为纯文本 const parsedText = parseHtmlToText(text); titles.push({ text, parsedText, id: item.id, depth: item.depth, needParse: text !== parsedText, }); // 递归处理子标题 if (item.count > 0) { titles = titles.concat(collectTitles(item.blocks ?? item.children)); } } return titles; }; // 生成大纲内容的函数 const generateOutline = async (nodeId, outlineContent) => { console.log("开始生成大纲,节点 ID:", nodeId); // 调试日志 try { if (!nodeId) { outlineContent.innerHTML = "<li>未找到有效节点,请点击文档内容。</li>"; return; } // 检查是否需要重新生成大纲 const currentOutlineNodeId = outlineContent.getAttribute('data-current-node-id'); if (currentOutlineNodeId === nodeId) { console.log("大纲已存在,无需重新生成"); // 调试日志 return; } const docOutline = await getDocOutline(nodeId); // 获取文档大纲 if (!docOutline) { outlineContent.innerHTML = "<li>无法获取文档目录结构。</li>"; return; } const titles = collectTitles(docOutline); // 收集大纲标题 const fragment = document.createDocumentFragment(); // 创建文档片段 // 遍历标题并生成列表项 titles.forEach(title => { const listItem = document.createElement("li"); const link = document.createElement("span"); link.setAttribute("data-type", "a"); link.setAttribute("data-href", `siyuan://blocks/${title.id}`); // 使用 textContent 设置内容,避免HTML被解析 link.textContent = title.parsedText; // 设置链接样式 Object.assign(link.style, { color: "#000", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", display: "inline-block", maxWidth: "100%", }); // 添加链接点击事件 link.addEventListener("mousedown", (e) => { if (e.button === 0) { e.preventDefault(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(link); selection.removeAllRanges(); selection.addRange(range); } }); link.addEventListener('click', function (event) { event.preventDefault(); const href = this.getAttribute("data-href"); window.open(href, '_blank'); }); // 设置列表项的缩进 listItem.style.paddingLeft = `${title.depth * 15}px`; listItem.appendChild(link); fragment.appendChild(listItem); }); // 清空并更新大纲内容 outlineContent.innerHTML = ""; outlineContent.appendChild(fragment); // 更新当前大纲的节点 ID outlineContent.setAttribute('data-current-node-id', nodeId); console.log("大纲生成完成"); // 调试日志 } catch (error) { outlineContent.innerHTML = `<li>生成大纲失败:${error.message}</li>`; } }; // ========== 页面交互函数 ========== // 修改函数:根据 rootId 查找并返回所有兄弟元素的 title 文本数组 function getLastSiblingTitle(rootId) { const allBreadcrumbItems = document.querySelectorAll('.protyle-breadcrumb__item'); for (const item of allBreadcrumbItems) { if (item.getAttribute('data-node-id') === rootId && item.classList.contains('protyle-breadcrumb__item--active')) { const parent = item.parentElement; const siblings = Array.from(parent.children).filter(sibling => sibling !== item); const titles = []; siblings.forEach(sibling => { const titleElement = sibling.querySelector('.protyle-breadcrumb__text'); if (titleElement && titleElement.getAttribute('title')) { titles.push(titleElement.getAttribute('title')); } }); return titles; // 返回包含所有兄弟节点标题的数组 } } return []; // 如果未找到符合条件的元素,返回空数组 } // 处理 NodeHeading 类型 function handleNodeHeading(outlineContent, nodeId) { console.log("处理 NodeHeading 类型,节点 ID:", nodeId); // 调试日志 highlightOutlineElements(outlineContent, nodeId, null); } // 处理非 NodeHeading 类型 function handleNonNodeHeading(outlineContent, textContentArray) { console.log("处理非 NodeHeading 类型,兄弟节点标题数组:", textContentArray); // 调试日志 if (Array.isArray(textContentArray) && textContentArray.length > 0) { // 倒序遍历数组,依次尝试匹配 for (let i = textContentArray.length - 1; i >= 0; i--) { const currentLine = textContentArray[i]; if (highlightOutlineElements(outlineContent, null, currentLine)) { return; // 匹配成功后退出循环 } } } } // 全局点击事件处理函数 const handleClick = async (e, outlineContent) => { console.log("点击事件触发,目标元素:", e.target); // 调试日志 let target = e.target; while (target && target !== document && !target.hasAttribute('data-node-id')) { target = target.parentNode; } if (target && target.hasAttribute('data-node-id')) { const nodeId = target.getAttribute('data-node-id'); try { // 获取 rootID const rootID = await getRootID({ id: nodeId }); if (rootID) { console.log("开始生成大纲,节点 ID:", nodeId); // 调试日志 await generateOutline(nodeId, outlineContent); if (target.getAttribute('data-type') === 'NodeHeading') { handleNodeHeading(outlineContent, nodeId); } else { const siblingTitles = getLastSiblingTitle(rootID); if (siblingTitles.length > 0) { handleNonNodeHeading(outlineContent, siblingTitles); } } } } catch (error) { console.error('获取 rootID 失败:', error.message); } } else { outlineContent.innerHTML = "<li>未找到有效节点,请点击文档内容。</li>"; } }; // ========== UI 创建函数 ========== // 创建大纲面板的函数 const createOutlinePanel = () => { const outlinePanel = document.createElement("div"); Object.assign(outlinePanel.style, { position: "fixed", top: "100px", left: "1000px", width: "200px", height: "30px", background: "#f1f1f1", border: "1px solid #ccc", borderRadius: "5px", padding: "0", boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", zIndex: "1000", overflow: "hidden", }); const topButtonsContainer = createTopButtonsContainer(); outlinePanel.appendChild(topButtonsContainer); const outlineTitle = document.createElement("h3"); outlineTitle.textContent = "大纲"; Object.assign(outlineTitle.style, { margin: "0 0 10px", }); outlinePanel.appendChild(outlineTitle); const outlineContent = document.createElement("ul"); outlineContent.id = "outline-list"; Object.assign(outlineContent.style, { listStyle: "none", padding: "0", margin: "0", fontSize: "14px", marginTop: "20px", overflowY: "auto", maxHeight: "340px", }); outlineContent.setAttribute("draggable", "false"); outlineContent.addEventListener("dragstart", (e) => e.preventDefault()); outlinePanel.appendChild(outlineContent); const toggleButtonInstance = topButtonsContainer.querySelector('button:nth-child(2)'); toggleButtonInstance.textContent = "展开"; let isExpanded = false; toggleButtonInstance.addEventListener("click", () => { isExpanded = !isExpanded; outlinePanel.style.height = isExpanded ? "400px" : `${topButtonsContainer.offsetHeight}px`; toggleButtonInstance.textContent = isExpanded ? "折叠" : "展开"; }); enableDragging(outlinePanel, topButtonsContainer); return { outlinePanel, outlineContent }; }; // 创建顶部按钮容器的函数 const createTopButtonsContainer = () => { const topButtonsContainer = document.createElement("div"); Object.assign(topButtonsContainer.style, { position: "absolute", top: "0", left: "0", right: "0", height: "20px", backgroundColor: "#f1f1f1", display: "flex", justifyContent: "space-between", alignItems: "center", padding: "5px", }); const showOutlineButton = document.createElement("button"); showOutlineButton.textContent = "思源大纲"; Object.assign(showOutlineButton.style, { padding: "5px", background: "#007bff", color: "#fff", border: "none", borderRadius: "5px", cursor: "pointer", }); const toggleButtonElement = document.createElement("button"); toggleButtonElement.textContent = "展开"; Object.assign(toggleButtonElement.style, { padding: "5px", background: "#ccc", border: "none", borderRadius: "5px", cursor: "pointer", }); topButtonsContainer.appendChild(showOutlineButton); topButtonsContainer.appendChild(toggleButtonElement); return topButtonsContainer; }; // 创建字体大小调整按钮的函数 const createFontButtonsContainer = (outlineContent) => { const fontButtonsContainer = document.createElement("div"); Object.assign(fontButtonsContainer.style, { display: "flex", gap: "5px", }); const decreaseFontSizeButton = document.createElement("button"); decreaseFontSizeButton.textContent = "-"; Object.assign(decreaseFontSizeButton.style, { width: "20px", height: "20px", fontSize: "16px", border: "none", background: "#ccc", borderRadius: "50%", }); decreaseFontSizeButton.addEventListener("click", () => { const currentSize = parseFloat(outlineContent.style.fontSize); outlineContent.style.fontSize = `${Math.max(currentSize - 1, 10)}px`; }); const increaseFontSizeButton = document.createElement("button"); increaseFontSizeButton.textContent = "+"; Object.assign(increaseFontSizeButton.style, { width: "20px", height: "20px", fontSize: "16px", border: "none", background: "#ccc", borderRadius: "50%", }); increaseFontSizeButton.addEventListener("click", () => { const currentSize = parseFloat(outlineContent.style.fontSize); outlineContent.style.fontSize = `${Math.min(currentSize + 1, 24)}px`; }); fontButtonsContainer.appendChild(decreaseFontSizeButton); fontButtonsContainer.appendChild(increaseFontSizeButton); return fontButtonsContainer; }; // 实现面板拖动功能的函数 const enableDragging = (outlinePanel, topButtonsContainer) => { let isDragging = false; let offsetX, offsetY; topButtonsContainer.style.cursor = "move"; topButtonsContainer.addEventListener("mousedown", (e) => { if (isDragging) return; isDragging = true; offsetX = e.clientX - Number(outlinePanel.style.left.replace('px', '')); offsetY = e.clientY - Number(outlinePanel.style.top.replace('px', '')); const onMouseMove = (e) => { if (!isDragging) return; let newX = e.clientX - offsetX; let newY = e.clientY - offsetY; const minX = 20; const minY = 20; const maxX = window.innerWidth - outlinePanel.offsetWidth - 20; const maxY = window.innerHeight - outlinePanel.offsetHeight - 20; newX = Math.max(minX, Math.min(newX, maxX)); newY = Math.max(minY, Math.min(newY, maxY)); outlinePanel.style.left = `${newX}px`; outlinePanel.style.top = `${newY}px`; }; const onMouseUp = () => { isDragging = false; document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); }; // ========== 主函数 ========== const main = () => { const { outlinePanel, outlineContent } = createOutlinePanel(); document.body.appendChild(outlinePanel); const fontButtonsContainer = createFontButtonsContainer(outlineContent); const topButtonsContainer = outlinePanel.querySelector('div'); topButtonsContainer.appendChild(fontButtonsContainer); document.addEventListener('click', (e) => handleClick(e, outlineContent)); }; main();
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于