思源笔记插件丨自定义排序模式下,进行一次性自动化排序

背景

思源笔记的文档树可以选择多种排序方式,因为根据文档名进行自动排序,往往不够自由灵活,所以我一般选择自定义排序。

但是在自定义排序模式下,有时候如果想要根据文件名排序,自己一个个拖实在太麻烦了,要是能在自定义排序的基础上,加上排序功能该多好。可以先随意无序创建文档,沉静于创作,之后再根据文档名进行排序,得到有序的笔记层级,非常的自由灵活。

image

提了一个 issue,自定义排序下,给笔记本和父文档右键菜单添加排序功能 · Issue #13297 · siyuan-note/siyuan

但是迅速被否了

于是只好自己先写一个简单的插件,先用用。

插件功能

Achuan-2/siyuan-plugin-doctree-autosort: Custom Sort + Automation Sort = Ideal DocTree Sort

当文档树排序模式为自定义排序时,插件会有文档树中的父文档和笔记本右键菜单添加排序按钮,点击即可根据名称字母进行自动化排序,解放双手。
目前只支持根据名称字母进行自然排序。

自定义排序 2

开发笔记

获取当前排序规则

window.siyuan.config.fileTree.sort =6 为自定义排序

PixPin_2024-11-28_11-45-26

调用 API 的方法


async function fetchSyncPost(url, data, returnType = 'json') {
    const init = {
        method: "POST",
    };
    if (data) {
        if (data instanceof FormData) {
            init.body = data;
        } else {
            init.body = JSON.stringify(data);
        }
    }
    try {
        const res = await fetch(url, init);
        const res2 = returnType === 'json' ? await res.json() : await res.text();
        return res2;
    } catch (e) {
        console.log(e);
        return returnType === 'json' ? { code: e.code || 1, msg: e.message || "", data: null } : "";
    }
}


获取当前文档信息

/api/filetree/getDoc​:获取 doc 信息,发现还能获取到 DOM 内容!

let parent_doc_id ="20241128153524-e8m8fzb";
let parent_doc_info = (await fetchSyncPost("/api/filetree/getDoc", {
            id: parent_doc_id
            // 默认最大加载块数 102400 
        })).data;

console.log(parent_doc_info)

PixPin_2024-11-28_15-13-44

/api/block/getDocInfo​:获取块信息,对于文档来说,只是基本的文档属性信息

// 查询文档信息
let parent_doc_id ="20241128153524-e8m8fzb";
let parent_doc_info = (await fetchSyncPost("/api/block/getDocInfo", {
            id: parent_doc_id
        })).data;

console.log(parent_doc_info)

PixPin_2024-11-28_15-43-16

如何获取当前文档的子文档

/api/filetree/listDocTree

let query = {
    "notebook": parent_doc_info.box,
    "path": parent_doc_info.path.replace(".sy","")
}

let child_doc_ids = (await fetchSyncPost("/api/filetree/listDocTree", query)).data.tree;
console.log(child_doc_ids);

获取得到 48 个 array,每个 array 的结构为 {id: '20220821202911-z8t1vmo', children: Array(11)}

PixPin_2024-11-28_15-28-48

根据 id 获取文档名,并进行排序

async function fetchNameById(id) {
    // Fetch the name for a given id
    const response = await fetchSyncPost("/api/filetree/getHPathByID", {id: id });
    return response.data; // Assuming the API returns the name in this structure
}


// Fetch names for each id
let idNamePairs = await Promise.all(child_doc_ids.map(async (doc) => {
    let name = await fetchNameById(doc.id);
    return { id: doc.id, name: name };
}));

// Sort the pairs by name
idNamePairs.sort((a, b) => a.name.localeCompare(b.name));

// Create a sorted result object
let sortedResult = {};
idNamePairs.forEach((pair, index) => {
    sortedResult[pair.id] = index; // Assuming you want the order to start from 1
});


console.log(sortedResult)

确认能否用 api 获取 sort.json 自定义排序文件

let query = {
 "path" : `/data/${parent_doc_info.box}/.siyuan/sort.json`
}

const sort_json = await fetchSyncPost('/api/file/getFile',query)

如何把修改过的排序值写入 sort.json 文件


// 保存文件的辅助函数
async function putFile(path, json, isDir = false, modTime = Date.now()) {
    let file;
    if (typeof json === "object") {
        file = new File(
            [new Blob([JSON.stringify(json)], { type: "application/json" })],
            path.split("/").pop()
        );
    } else {
        file = new File([new Blob([json])], path.split("/").pop());
    }

    let formdata = new FormData();
    formdata.append("path", path);
    formdata.append("file", file);
    formdata.append("isDir", isDir);
    formdata.append("modTime", modTime);
  
    const response = await fetch("/api/file/putFile", {
        body: formdata,
        method: "POST",
        headers: {
            Authorization: `Token `,
        },
    });
  
    if (response.ok)
        return await response.json();
    else 
        return null;
}
// 保存更新后的sort.json文件
await putFile(`/data/${parentDocInfo.box}/.siyuan/sort.json`, sortJson);

如何刷新文档树

/api/filetree/refreshFiletree​重建索引,太过麻烦,没有必要

✅ 点击折叠再展开即可刷新排序后的文档

在开发者工具里实现排序功能

用法:修改 parentDocId,直接粘贴到开发者工具里(对,没有写成代码片段)。

// 调用函数进行排序
const parentDocId = "20241128153524-e8m8fzb";
const result = await sortChildDocsByID(parentDocId);
if (result) {
    console.log('排序成功');
} else {
    console.log('排序失败');
}


async function sortChildDocsByID(parentDocId) {
    // 判断是否是自定义排序模式
    if (window.siyuan.config.fileTree.sort != 6){

        return 
    }
    // 通用的fetch函数
    async function fetchSyncPost(url, data, returnType = 'json') {
        const init = {
            method: "POST",
        };
        if (data) {
            if (data instanceof FormData) {
                init.body = data;
            } else {
                init.body = JSON.stringify(data);
            }
        }
        try {
            const res = await fetch(url, init);
            const res2 = returnType === 'json' ? await res.json() : await res.text();
            return res2;
        } catch (e) {
            console.log(e);
            return returnType === 'json' ? { code: e.code || 1, msg: e.message || "", data: null } : "";
        }
    }

    // 获取文档名称的辅助函数
    async function fetchNameById(id) {
        const response = await fetchSyncPost("/api/filetree/getHPathByID", { id: id });
        return response.data;
    }

    // 保存文件的辅助函数
    async function putFile(path, json, isDir = false, modTime = Date.now()) {
        let file;
        if (typeof json === "object") {
            file = new File(
                [new Blob([JSON.stringify(json)], { type: "application/json" })],
                path.split("/").pop()
            );
        } else {
            file = new File([new Blob([json])], path.split("/").pop());
        }

        let formdata = new FormData();
        formdata.append("path", path);
        formdata.append("file", file);
        formdata.append("isDir", isDir);
        formdata.append("modTime", modTime);
      
        const response = await fetch("/api/file/putFile", {
            body: formdata,
            method: "POST",
            headers: {
                Authorization: `Token `,
            },
        });
      
        if (response.ok)
            return await response.json();
        else 
            return null;
    }

    try {
        // 1. 获取父文档信息
        const parentDocInfo = (await fetchSyncPost("/api/filetree/getDoc", {
            id: parentDocId
        })).data;

        // 2. 获取子文档列表
        const listDocTreeQuery = {
            "notebook": parentDocInfo.box,
            "path": parentDocInfo.path.replace(".sy", "")
        };
        const childDocIds = (await fetchSyncPost("/api/filetree/listDocTree", listDocTreeQuery)).data.tree;

        // 3. 获取所有文档名称并排序
        const idNamePairs = await Promise.all(childDocIds.map(async (doc) => {
            const name = await fetchNameById(doc.id);
            return { id: doc.id, name: name };
        }));
        idNamePairs.sort((a, b) => a.name.localeCompare(b.name));

        // 4. 创建排序结果对象
        const sortedResult = {};
        idNamePairs.forEach((pair, index) => {
            sortedResult[pair.id] = index;
        });

        // 5. 获取现有的sort.json文件
        const getFileQuery = {
            "path": `/data/${parentDocInfo.box}/.siyuan/sort.json`
        };
        const sortJson = await fetchSyncPost('/api/file/getFile', getFileQuery);

        // 6. 更新排序值
        for (let id in sortedResult) {
            if (sortJson.hasOwnProperty(id)) {
                sortJson[id] = sortedResult[id];
            }
        }

        // 7. 保存更新后的sort.json文件
        await putFile(`/data/${parentDocInfo.box}/.siyuan/sort.json`, sortJson);

        // 8. 刷新文档树
        const element = document.querySelector(`.file-tree li[data-node-id="${parentDocId}"] > .b3-list-item__toggle--hl`);
        if (element) {
            element.click();
            element.click();
        }

        return true;
    } catch (error) {
        console.error('排序过程中出现错误:', error);
        return false;
    }
}

如何获取文档树右键点击事件,并添加菜单

监听文档树右键菜单

this.eventBus.on("open-menu-doctree", this.addSortButton.bind(this));

添加菜单

文档树可以选中多个文件,所以是 elements 而不是 element,暂时设置只选中一个文档时且这个文档是父文档或者笔记本,右键才添加菜单.

判断是否是笔记本,是 elements 有 data-type="navigation-root"属性,父节点有 data-url,为笔记本 id,给 sortDocuments 传递笔记本 id 或者父文档 id,并添加一个参数,isNotebook

private addSortButton = ({ detail }: any) => {
    const elements = detail.elements;
    if (elements.length === 1) {
        const element = elements[0];
        const isParentDoc = element.getAttribute("data-count") > 0;
        const isNotebook = element.getAttribute("data-type") === "navigation-root";
        const id = isNotebook ? element.parentNode.getAttribute("data-url") : element.getAttribute("data-id");

        if (isParentDoc || isNotebook) {
            detail.menu.addItem({
                icon: "iconSort",
                label: "Sort Documents",
                click: () => this.sortDocuments(id, isNotebook)
            });
        }
    }
}

自然排序实现

Intl.Collator

  • numeric​ 设置为 true​,Intl.Collator​ 会识别字符串中的数字,并按照数值大小进行排序。
  • sensitivity​ 设置为 "base"​ 时,比较过程只考虑基本字母的差异,而忽略重音符号和大小写。

举例;

 var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
var myArray = ['1_Document', '11_Document', '2_Document'];
console.log(myArray.sort(collator.compare));

['1_Document', '2_Document', '11_Document']

本插件用的代码

const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
idNamePairs.sort((a, b) => ascending ? collator.compare(a.name, b.name) : collator.compare(b.name, a.name));
// 创建排序结果对象
const sortedResult = {};
idNamePairs.forEach((pair, index) => {
    sortedResult[pair.id] = index;
});
  • 思源笔记

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

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

    22478 引用 • 90064 回帖 • 1 关注
3 操作
Achuan-2 在 2024-11-28 23:17:47 更新了该帖
Achuan-2 在 2024-11-28 23:17:08 更新了该帖
Achuan-2 在 2024-11-28 17:35:57 更新了该帖

相关帖子

欢迎来到这里!

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

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