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

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

由于刚接触思源,插件还不会写,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 片段怎么样?

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

  • 思源笔记

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

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

    21201 引用 • 83539 回帖
  • 插件
    92 引用 • 511 回帖 • 3 关注
6 操作
wilsons 在 2024-07-27 23:06:47 更新了该帖
wilsons 在 2024-07-27 19:10:51 更新了该帖
wilsons 在 2024-07-27 18:58:52 更新了该帖
wilsons 在 2024-07-27 17:04:21 更新了该帖 wilsons 在 2024-07-26 23:14:17 更新了该帖 wilsons 在 2024-07-24 06:29:40 更新了该帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • 新人

    让我们欢迎这对新人。哦,不好意思说错了,让我们欢迎这位新人!
    新手上路,请谨慎驾驶!

    51 引用 • 226 回帖
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    107 引用 • 152 回帖 • 1 关注
  • FreeMarker

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

    23 引用 • 20 回帖 • 454 关注
  • Oracle

    Oracle(甲骨文)公司,全称甲骨文股份有限公司(甲骨文软件系统有限公司),是全球最大的企业级软件公司,总部位于美国加利福尼亚州的红木滩。1989 年正式进入中国市场。2013 年,甲骨文已超越 IBM,成为继 Microsoft 后全球第二大软件公司。

    104 引用 • 126 回帖 • 422 关注
  • 区块链

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

    91 引用 • 751 回帖
  • OpenStack

    OpenStack 是一个云操作系统,通过数据中心可控制大型的计算、存储、网络等资源池。所有的管理通过前端界面管理员就可以完成,同样也可以通过 Web 接口让最终用户部署资源。

    10 引用 • 1 关注
  • PostgreSQL

    PostgreSQL 是一款功能强大的企业级数据库系统,在 BSD 开源许可证下发布。

    22 引用 • 22 回帖 • 2 关注
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 200 关注
  • Docker

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。

    490 引用 • 914 回帖
  • 人工智能

    人工智能(Artificial Intelligence)是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门技术科学。

    129 引用 • 171 回帖
  • VirtualBox

    VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。

    10 引用 • 2 回帖 • 17 关注
  • Google

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

    49 引用 • 192 回帖 • 1 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    103 引用 • 295 回帖
  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    198 引用 • 120 回帖
  • 外包

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

    26 引用 • 232 回帖
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    130 引用 • 793 回帖
  • CentOS

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

    238 引用 • 224 回帖
  • Scala

    Scala 是一门多范式的编程语言,集成面向对象编程和函数式编程的各种特性。

    13 引用 • 11 回帖 • 117 关注
  • IPFS

    IPFS(InterPlanetary File System,星际文件系统)是永久的、去中心化保存和共享文件的方法,这是一种内容可寻址、版本化、点对点超媒体的分布式协议。请浏览 IPFS 入门笔记了解更多细节。

    21 引用 • 245 回帖 • 247 关注
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    90 引用 • 59 回帖 • 6 关注
  • Java

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

    3170 引用 • 8209 回帖
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 28 关注
  • Latke

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

    70 引用 • 533 回帖 • 760 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 663 关注
  • RIP

    愿逝者安息!

    8 引用 • 92 回帖 • 341 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    287 引用 • 4484 回帖 • 670 关注
  • 阿里云

    阿里云是阿里巴巴集团旗下公司,是全球领先的云计算及人工智能科技公司。提供云服务器、云数据库、云安全等云计算服务,以及大数据、人工智能服务、精准定制基于场景的行业解决方案。

    89 引用 • 345 回帖