js 片段实现目录树自动展开 V0.0.2 版

本贴最后更新于 315 天前,其中的信息可能已经渤澥桑田

注意:

从 v3.1.7 开始支持已经支持记住文档树展开状态了,参见 持久化文档树展开状态


看到论坛里有很多人表示希望有这个功能,今天也在看帖子讨论的过程中研究了一番,有一点思路,就试着实现了下。

由于刚接触思源,插件还不会写,api 也不熟,所以选择用 js 片段实现,基本是纯原生 js 实现。

主要功能: 记住上次已展开的目录,下次打开或刷新时自动展开上次已展开的目录。

实现原理:

仅记录已展开目录的 data-path, 笔记以 data-url 区分,以 object 嵌套的方式记录展开目录所在的层级关系。

加载时,会根据记录的目录层级关系,依次展开。

展开时,会记录目录的层级关系,如果不存在则添加。

折叠时,会根据目录的层级关系,删除对应的 Object 嵌套层级。

数据默认存储在/data/storage/tree-auto-extend.json 中,object 嵌套格式可参考这里的数据。

版本更新:

目前是 V0.0.2,支持 pc 客户端,web 版和手机版。但注意,pc 发布版是只读模式,无法写入数据,因此无法使用,请使用伺服版。

V0.0.2 更新内容: 更新了目录被删除或移动后可能报错的问题,兼容手机版和 web 版。

能力有限,这里仅仅是抛砖引玉,完善和优化还得靠各位大佬的鼎力支持!

完整代码如下:

// 目录树自动展开 // 功能:记住上次已展开的目录,下次打开或刷新时自动展开上次已展开的目录 // Version 0.0.2 // By Wilson 2024.7.24 晚,修改于 2024.7.26 晚 // MIT License // 0.0.2 更新了目录被删除或移动后可能报错的问题,兼容手机版和web版 // 实现原理: // 仅记录已展开目录的data-path, 笔记以data-url区分,以object嵌套的方式记录展开目录所在的层级关系 // 加载时,会根据记录的目录层级关系,依次展开 // 展开时,会记录目录的层级关系,如果不存在则添加 // 折叠时,会根据目录的层级关系,删除对应的Object嵌套层级 // 目前支持pc客户端,web版和手机版,但注意,pc发布版是只读模式,无法写入数据,因此无法使用,请使用伺服版。 // 配置存储路径(这个存储路径可以根据你自己的需要进行修改) const storagePath = "/data/storage/tree-auto-extend.json"; // 是否启用自动定位当前文件所在的目录,启用后当前焦点文档所在的目录也会自动展开 const isShowFilePosition = true; // 监听笔记列表渲染完成 whenElementExist("ul.b3-list[data-url]").then(async myElement => { // 防止更多笔记列表尚未渲染完成(笔记目录只渲染第一级,一般20毫秒足够了) await sleep(20); // 获取所有展开元素(这里变量命名有点问题,就这样吧) let collapsedEls = await getCollapsedEls(); // 监听折叠事件 document.querySelectorAll("ul.b3-list[data-url]").forEach(async book => { // 当前笔记的 data-url const bookDataUrl = book.getAttribute("data-url"); // 自动展开 autoExpandElements(collapsedEls[bookDataUrl], bookDataUrl); // 监听折叠事件 book.addEventListener("click", async event => { //等待箭头按钮渲染完成 // await sleep(40); let arrowBtn = event.target; if (event.target.tagName == "SPAN") { arrowBtn = event.target.firstElementChild; } // 如果是箭头按钮 if (arrowBtn && arrowBtn.classList.contains("b3-list-item__arrow")) { //等待箭头按钮切换完成 const currentClassList = arrowBtn.classList.toString(); await whenElementExist(()=>{ return arrowBtn.classList.toString() !== currentClassList; }); const isOpen = arrowBtn.classList.contains("b3-list-item__arrow--open"); // 存储当前元素的折叠状态 storeElement(collapsedEls, bookDataUrl, arrowBtn, isOpen); } }); }); // 定位当前打开的文档 if(isShowFilePosition){ await sleep(40); document.querySelector(".layout-tab-container .block__icons span[data-type=focus]")?.click(); } }); // 自动展元素 async function autoExpandElements(collapsedEls, bookDataUrl, path = []) { if (!collapsedEls) { return; } // 循环所有存储的展开元素并自动展开 for (let key in collapsedEls) { if (key && collapsedEls.hasOwnProperty(key)) { const newPath = [...path, key]; const joinPath = newPath.join('/'); // 意外情况跳过 if (joinPath === "") continue; const elPath = joinPath === "/" ? "/" : `/${joinPath.replace(/^\/+/, '')}.sy`; const elArrowBtn = document.querySelector("ul.b3-list[data-url='" + bookDataUrl + "'] li[data-path='" + elPath + "'] span.b3-list-item__toggle"); if (elArrowBtn && !elArrowBtn.firstElementChild.classList.contains("b3-list-item__arrow--open")) { elArrowBtn.click(); // 等待子元素渲染完毕 //await sleep(40); await whenElementExist(()=>{ return elArrowBtn.closest("li").nextElementSibling?.tagName === 'UL'; }); } // 如果当前值也是一个对象,递归遍历 if (typeof collapsedEls[key] === 'object' && collapsedEls[key] !== null && !Array.isArray(collapsedEls[key])) { autoExpandElements(collapsedEls[key], bookDataUrl, newPath); } } } } // 存储当前展开的元素 function storeElement(collapsedEls, bookDataUrl, arrowBtn, isOpen) { const nodePath = arrowBtn.parentElement.parentElement.getAttribute("data-path"); const nodePathArr = nodePath === "/" ? ["/"] : nodePath.replace(/.sy$/i, '').split("/"); nodePathArr[0] = nodePathArr[0] === "" ? "/" : nodePathArr[0]; if (!collapsedEls[bookDataUrl]) { collapsedEls[bookDataUrl] = {}; } if (isOpen) { // 如果是展开,则创建嵌套对象 createNestedObject(nodePathArr, collapsedEls[bookDataUrl]); } else { // 如果是折叠,则删除最后一个key removeNestedKey(collapsedEls, bookDataUrl, nodePathArr); } // 存储展开元素的数据 storeCollapsedEl(collapsedEls); } // 获取存储的展开元素(这里函数命名有点问题,就这样吧) async function getCollapsedEls() { let data = await getFileContent(storagePath||"/data/storage/tree-auto-extend.json") || "{}"; data = JSON.parse(data); if(data.code === 404) data = {}; return data; } // 存储展开元素(这里函数命名有点问题,就这样吧) function storeCollapsedEl(data) { putFileContent(storagePath||"/data/storage/tree-auto-extend.json", JSON.stringify(data)) } // 创建嵌套对象 function createNestedObject(pathArray, objRef) { if (pathArray.length === 0) return; // 如果数组为空,退出递归 const currentNode = pathArray.shift(); // 取出数组的第一个元素 if (!objRef[currentNode]) { objRef[currentNode] = {}; // 创建一个新的空对象 } if (pathArray.length > 0) { createNestedObject(pathArray, objRef[currentNode]); // 递归调用,传入新的路径数组和当前节点的子对象 } } // 删除嵌套对象的最后一个key function removeNestedKey(obj, urlKey, pathArr) { if (!obj || !urlKey || !pathArr || pathArr.length === 0) { return; } let currentLevel = obj[urlKey]; if (!currentLevel) { return; } // Traverse through the object based on the path array for (let i = 0; i < pathArr.length - 1; i++) { const key = pathArr[i]; if (!currentLevel[key]) { return; } currentLevel = currentLevel[key]; } // Delete the last key specified in the path array const lastKeyToDelete = pathArr[pathArr.length - 1]; if (lastKeyToDelete in currentLevel) { delete currentLevel[lastKeyToDelete]; } } // 延迟执行 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 等待元素渲染完成后执行 function whenElementExist(selector) { return new Promise(resolve => { const checkForElement = () => { let isExist = false; if (typeof selector === 'function') { isExist = selector(); } else { isExist = document.querySelector(selector); } if (isExist) { resolve(true); } else { requestAnimationFrame(checkForElement); } }; checkForElement(); }); } // 写入文件内容 async function putFileContent(path, content) { const formData = new FormData(); formData.append("path", path); formData.append("file", new Blob([content])); return fetch("/api/file/putFile", { method: "POST", body: formData, }) .then((response) => { if (response.ok) { //console.log("File saved successfully"); } else { throw new Error("Failed to save file"); } }) .catch((error) => { console.error(error); }); } // 获取文件内容 async function getFileContent(path) { return fetch("/api/file/getFile", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ path, }), }) .then((response) => { if (response.ok) { return response.text(); } else { throw new Error("Failed to get file content"); } }) .catch((error) => { console.error(error); }); }

使用方法:

  1. 设置 -> 外观 -> 代码片段 -> JS 中添加新的 js 代码片段即可。

  2. 输入上述代码到你新建的代码片段中

  3. 开启代码片段即可(如果有问题重启思源试试)

    image.png

注意: 如果你开启了 const isShowFilePosition = true,当前焦点文档所在的目录也会自动展开。


说明: 可能有时事情较多,可能无法及时看到大家的问题,不过在空闲时,会第一时间回复,请大家积极踊跃提出宝贵意见!


你觉得这个 js 片段怎么样?

单选 公开 永不结束 12 票
非常棒 👍
100% 12 票
一般般啦
0% 0 票
不好用
0% 0 票

  • 思源笔记

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

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

    25857 引用 • 107086 回帖 • 1 关注
  • 插件
    102 引用 • 630 回帖 • 3 关注

相关帖子

欢迎来到这里!

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

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

    我试了下, 这个功能好像只有: 保存展开情况, 刷新后恢复

    但我感觉论坛讨论的还有几个需求

    1. 展开多级, 然后点击中间层级的折叠, 最后再点击展开, 最底层的还是展开的
    2. 新建文档之后, 不要折叠底层的文档

    不过思路很有意思, 我准备抄一抄, 周末按照我的需求, 改吧改吧

    1 回复
  • 其他回帖
  • wilsons 1

    明白了,确实两者都能自适应,不过,必须从 url 根路径访问,比如:htttp://192.163.0.103:6806/

    我之前是因为,先 pc 打开地址,然后手机扫描二维码,那么地址就会变为:htttp://192.163.0.103:6806/stage/build/desktop/ ,然后,因为有了 desktop 后缀,这时候无论手机或 pc 访问都是访问的是桌面版的 UI,同理手机版也是,后缀是/mobile/。

    所以,手动改下后缀再访问也是一样的,比如:htttp://192.163.0.103:6806/stage/build/mobile/ 访问手机版。

  • wilsons 1

    感谢建议!已发。

  • 查看全部回帖
wilsons
正在努力开发 wilsons 工具箱中 🛠️ 目前已正式入驻爱发电啦!💖 想催更、提需求?欢迎访问 👉 https://afdian.com/a/wilsons

推荐标签 标签

  • 机器学习

    机器学习(Machine Learning)是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。

    77 引用 • 37 回帖
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 433 关注
  • 强迫症

    强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。

    15 引用 • 161 回帖 • 3 关注
  • 运维

    互联网运维工作,以服务为中心,以稳定、安全、高效为三个基本点,确保公司的互联网业务能够 7×24 小时为用户提供高质量的服务。

    151 引用 • 257 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3201 引用 • 8216 回帖 • 5 关注
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 112 关注
  • CloudFoundry

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

    5 引用 • 18 回帖 • 186 关注
  • 30Seconds

    📙 前端知识精选集,包含 HTML、CSS、JavaScript、React、Node、安全等方面,每天仅需 30 秒。

    • 精选常见面试题,帮助您准备下一次面试
    • 精选常见交互,帮助您拥有简洁酷炫的站点
    • 精选有用的 React 片段,帮助你获取最佳实践
    • 精选常见代码集,帮助您提高打码效率
    • 整理前端界的最新资讯,邀您一同探索新世界
    488 引用 • 384 回帖 • 7 关注
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    49 引用 • 192 回帖
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 119 关注
  • Firefox

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

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

    OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。

    36 引用 • 103 回帖 • 33 关注
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 121 关注
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖
  • 区块链

    区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。

    92 引用 • 752 回帖 • 1 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    693 引用 • 537 回帖
  • Love2D

    Love2D 是一个开源的, 跨平台的 2D 游戏引擎。使用纯 Lua 脚本来进行游戏开发。目前支持的平台有 Windows, Mac OS X, Linux, Android 和 iOS。

    14 引用 • 53 回帖 • 556 关注
  • 反馈

    Communication channel for makers and users.

    122 引用 • 912 回帖 • 280 关注
  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    167 引用 • 408 回帖 • 490 关注
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 58 关注
  • Maven

    Maven 是基于项目对象模型(POM)、通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。

    188 引用 • 319 回帖 • 247 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    284 引用 • 248 回帖 • 2 关注
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 610 关注
  • 大数据

    大数据(big data)是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。

    89 引用 • 113 回帖 • 2 关注
  • GraphQL

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

    4 引用 • 3 回帖 • 13 关注
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    45 引用 • 114 回帖 • 177 关注