背景
思源笔记的文档树可以选择多种排序方式,因为根据文档名进行自动排序,往往不够自由灵活,所以我一般选择自定义排序。
但是在自定义排序模式下,有时候如果想要根据文件名排序,自己一个个拖实在太麻烦了,要是能在自定义排序的基础上,加上排序功能该多好。可以先随意无序创建文档,沉静于创作,之后再根据文档名进行排序,得到有序的笔记层级,非常的自由灵活。
提了一个 issue,自定义排序下,给笔记本和父文档右键菜单添加排序功能 · Issue #13297 · siyuan-note/siyuan
但是迅速被否了
于是只好自己先写一个简单的插件,先用用。
插件功能
Achuan-2/siyuan-plugin-doctree-autosort: Custom Sort + Automation Sort = Ideal DocTree Sort
当文档树排序模式为自定义排序时,插件会有文档树中的父文档和笔记本右键菜单添加排序按钮,点击即可根据名称字母进行自动化排序,解放双手。
目前只支持根据名称字母进行自然排序。
开发笔记
获取当前排序规则
window.siyuan.config.fileTree.sort =6 为自定义排序
调用 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)
❌/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)
如何获取当前文档的子文档
/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)}
根据 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;
});
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于