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 票

  • 思源笔记

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

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

    21208 引用 • 83583 回帖
  • 插件
    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 更新了该帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • 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/ 访问手机版。

  • 其他回帖
  • EmberSky

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

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

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

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

    1 回复
  • wilsons 1

    感谢建议!已发。

  • 查看全部回帖

推荐标签 标签

  • V2Ray
    1 引用 • 15 回帖 • 1 关注
  • 又拍云

    又拍云是国内领先的 CDN 服务提供商,国家工信部认证通过的“可信云”,乌云众测平台认证的“安全云”,为移动时代的创业者提供新一代的 CDN 加速服务。

    21 引用 • 37 回帖 • 535 关注
  • 锤子科技

    锤子科技(Smartisan)成立于 2012 年 5 月,是一家制造移动互联网终端设备的公司,公司的使命是用完美主义的工匠精神,打造用户体验一流的数码消费类产品(智能手机为主),改善人们的生活质量。

    4 引用 • 31 回帖 • 2 关注
  • PHP

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

    179 引用 • 407 回帖 • 499 关注
  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    497 引用 • 1387 回帖 • 315 关注
  • Telegram

    Telegram 是一个非盈利性、基于云端的即时消息服务。它提供了支持各大操作系统平台的开源的客户端,也提供了很多强大的 APIs 给开发者创建自己的客户端和机器人。

    5 引用 • 35 回帖
  • WebComponents

    Web Components 是 W3C 定义的标准,它给了前端开发者扩展浏览器标签的能力,可以方便地定制可复用组件,更好的进行模块化开发,解放了前端开发者的生产力。

    1 引用 • 1 关注
  • Flutter

    Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。 Flutter 可以与现有的代码一起工作,它正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。

    39 引用 • 92 回帖 • 9 关注
  • RIP

    愿逝者安息!

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

    Python 是一种面向对象、直译式电脑编程语言,具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法简捷和清晰,尽量使用无异义的英语单词,与其它大多数程序设计语言使用大括号不一样,它使用缩进来定义语句块。

    540 引用 • 672 回帖
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 31 关注
  • Angular

    AngularAngularJS 的新版本。

    26 引用 • 66 回帖 • 531 关注
  • VirtualBox

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

    10 引用 • 2 回帖 • 15 关注
  • 音乐

    你听到信仰的声音了么?

    60 引用 • 511 回帖
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 622 关注
  • SendCloud

    SendCloud 由搜狐武汉研发中心孵化的项目,是致力于为开发者提供高质量的触发邮件服务的云端邮件发送平台,为开发者提供便利的 API 接口来调用服务,让邮件准确迅速到达用户收件箱并获得强大的追踪数据。

    2 引用 • 8 回帖 • 465 关注
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖
  • TensorFlow

    TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。

    20 引用 • 19 回帖 • 1 关注
  • InfluxDB

    InfluxDB 是一个开源的没有外部依赖的时间序列数据库。适用于记录度量,事件及实时分析。

    2 引用 • 65 关注
  • AngularJS

    AngularJS 诞生于 2009 年,由 Misko Hevery 等人创建,后为 Google 所收购。是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入等。2.0 版本后已经改名为 Angular。

    12 引用 • 50 回帖 • 469 关注
  • wolai

    我来 wolai:不仅仅是未来的云端笔记!

    2 引用 • 14 回帖 • 1 关注
  • jsDelivr

    jsDelivr 是一个开源的 CDN 服务,可为 npm 包、GitHub 仓库提供免费、快速并且可靠的全球 CDN 加速服务。

    5 引用 • 31 回帖 • 49 关注
  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖
  • Docker

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

    490 引用 • 914 回帖
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    7551 引用 • 34367 回帖 • 198 关注
  • Laravel

    Laravel 是一套简洁、优雅的 PHP Web 开发框架。它采用 MVC 设计,是一款崇尚开发效率的全栈框架。

    20 引用 • 23 回帖 • 721 关注
  • 安全

    安全永远都不是一个小问题。

    200 引用 • 816 回帖