思源笔记可移动的导航大纲:js 代码片段

导航目录大体功能:(使用方法:把代码放进 js 代码片段,粘贴完注意第 8 条)

  1. 随着笔记标签页的切换,导航内容也能相应切换
  2. 目录字体大小可调
  3. 可折叠起来
  4. 导航目录过长,可滚动
  5. 鼠标点击目录,可以导航到正文相应位置
  6. 鼠标悬停在目录上,可弹出浮窗
  7. 鼠标单击块,浮动大纲中对应的标题内容颜色会改变。再次点击会找上一级标题
  8. image.png

看下图,应该能明白。更新时间 20250227,修复了一些 bug

screenshots.gif

// ========== 工具函数 ========== // 使用原生DOMParser解析HTML并提取纯文本内容 const parseHtmlToText = (html) => { return html.replace(/<[^>]+>/g, '').replace(/&nbsp/g, ' '); // 注意,使用时要在&nbsp后面加一个分号; }; // 通用高亮函数,返回是否匹配成功 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();
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    25355 引用 • 104713 回帖 • 1 关注
2 操作
cxg318 在 2025-02-27 01:25:05 更新了该帖
cxg318 在 2025-02-24 00:11:08 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • LitBearPibs

    大佬赶紧上架吧,插件市场怎么搜到,指个路子啊

  • 其他回帖
  • att88 2 评论

    我个人可太需要这个了好嘛!之前论坛找了好几次是否有悬浮大纲悬浮文档树,结果都是没有。思源大纲虽说可以四处移动,但是高宽还是受到主页面限制,有时候大纲没几行内容还占了一大片,老按快捷键开关也很麻烦。就很希望能像 ps 那样工具面板都能页面外悬浮,能专注页面本身不被视觉中心外的东西干扰。感谢作者!希望尽快上架

    文档树原来就是可以悬浮的,但悬浮大纲这个是真没有
    Floria233
    请问文档树怎么悬浮?
    att88
  • att88

    js 代码用了好像没起作用?上面 docker 栏中也没找到相应图标,是不是我哪里使用姿势不对

    1 回复
  • Floria233

    这个很棒啊,感谢大大。就我的理解,这个设计是不是和 ob 的 floating index(好像是这个)插件相似?就是可以使大纲悬浮起来——就动图与说明看起来,似乎是这个样子?

    (我就说嘛,ob 能做到的东西,思源大差不差也都能做到类似的,就是这边开发者人手缺乏罢了,扼腕……)

    这个我所能想到的第一个优点就是节省面板位置,因为插件那么多,而软件本体除了编辑器之外的其他部分又那么小,可谓寸土寸金,参考我的话,左边是自带文档树 + 书签两个部件,右边是二级文档树这个部件,这三个是长期固定,常用的浮动面板是 knote 和小记,偶尔需要查看大纲,就得按快捷键切换了(因为不是每个文档都有大纲,需要用的时候才会固定,不用的时候就不希望它占用位置)

    因为文档跳转频繁,文档树与其浮动还是贴边吧,而浮动的大纲面板可以按照需要跳出来,最能辅助正文查看又能灵活调取(不至于总是放在某个地方),唯有它浮动起来是最合适的 😁

    我的体验是,它是一个有则最好,无则稍憾(思源本体可以凑合)而绝非鸡肋的功能设计,ob,flowus(notion 不知道)还有有道云(印象不知道)这种都有特别给出这个设计啊,所以它绝对是有必要的

    目前我所想到的优化方向,1 设计上,应该是这个面板先跟主题风格和谐一致,除此之外,2 这个 UI 的信息界面,大大要不要参考动图右边那个大纲插件设计?给每个大标题旁边都用数字标注其下小标题的数量之类的?

    另,有道云的悬浮大纲,在不用时会变成一道道长短不一的横线,这个看起来很简洁,但可以的话,请大大千万别做成这样,因为我用过几次,每次都需要鼠标浮过去它才会显现,不够直观还增加了操作步骤。

    也不希望完全参照 ob 的那个插件——只能在一个固定位置悬浮(这个当大纲标题太长时,偶尔会和正文重合起来),就是动图中这种自由拖动我感觉挺好的 👍(不过它是只能在编辑区拖动?还是可以在整个界面拖动呢?后者感觉更好一点)

    其实这个面板,只要存在了就是它最大的意义,其他都是锦上添花(来自小白的看法)

    1 回复
  • 查看全部回帖

推荐标签 标签

  • 国际化

    i18n(其来源是英文单词 internationalization 的首末字符 i 和 n,18 为中间的字符数)是“国际化”的简称。对程序来说,国际化是指在不修改代码的情况下,能根据不同语言及地区显示相应的界面。

    8 引用 • 26 回帖 • 3 关注
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 233 回帖 • 4 关注
  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    952 引用 • 944 回帖 • 1 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    948 引用 • 1460 回帖 • 1 关注
  • OpenCV
    15 引用 • 36 回帖 • 5 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    7 引用 • 30 回帖 • 384 关注
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 1 关注
  • Word
    13 引用 • 41 回帖
  • RIP

    愿逝者安息!

    8 引用 • 92 回帖 • 399 关注
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖 • 1 关注
  • FreeMarker

    FreeMarker 是一款好用且功能强大的 Java 模版引擎。

    23 引用 • 20 回帖 • 463 关注
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    71 引用 • 535 回帖 • 830 关注
  • 叶归
    6 引用 • 17 回帖 • 14 关注
  • CentOS

    CentOS(Community Enterprise Operating System)是 Linux 发行版之一,它是来自于 Red Hat Enterprise Linux 依照开放源代码规定释出的源代码所编译而成。由于出自同样的源代码,因此有些要求高度稳定的服务器以 CentOS 替代商业版的 Red Hat Enterprise Linux 使用。两者的不同在于 CentOS 并不包含封闭源代码软件。

    239 引用 • 224 回帖 • 1 关注
  • 以太坊

    以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。

    34 引用 • 367 回帖 • 4 关注
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    6 引用 • 141 回帖
  • Mac

    Mac 是苹果公司自 1984 年起以“Macintosh”开始开发的个人消费型计算机,如:iMac、Mac mini、Macbook Air、Macbook Pro、Macbook、Mac Pro 等计算机。

    168 引用 • 597 回帖 • 2 关注
  • Excel
    31 引用 • 28 回帖
  • QQ

    1999 年 2 月腾讯正式推出“腾讯 QQ”,在线用户由 1999 年的 2 人(马化腾和张志东)到现在已经发展到上亿用户了,在线人数超过一亿,是目前使用最广泛的聊天软件之一。

    45 引用 • 557 回帖
  • CloudFoundry

    Cloud Foundry 是 VMware 推出的业界第一个开源 PaaS 云平台,它支持多种框架、语言、运行时环境、云平台及应用服务,使开发人员能够在几秒钟内进行应用程序的部署和扩展,无需担心任何基础架构的问题。

    5 引用 • 18 回帖 • 181 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    28 引用 • 197 回帖 • 30 关注
  • 电影

    这是一个不能说的秘密。

    122 引用 • 608 回帖
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 4 关注
  • JRebel

    JRebel 是一款 Java 虚拟机插件,它使得 Java 程序员能在不进行重部署的情况下,即时看到代码的改变对一个应用程序带来的影响。

    26 引用 • 78 回帖 • 676 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 342 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 634 关注
  • 旅游

    希望你我能在旅途中找到人生的下一站。

    95 引用 • 901 回帖 • 2 关注