文档树自动展开_release_v1.0
写在前面(重要!重要!重要!)
- 抄的大佬的代码: https://ld246.com/article/1721771759337
- 我只简单测了一下, 肯定有 bug, 欢迎反馈 pc 端的 bug
- 其他设备如果有问题, 请自行排查(我没需求), 手动狗头
- 平时没啥时间, bug 和开发中功能, 有时间再搞
- 帖子不会更新了, 后续有更新会放到 回帖 里, 请随意食用
- 实现原理比较简单粗暴, 如果文档比较多, 会有性能问题, 如果是性能问题, 请自行优化(我没需求)
当前功能
- 记录文档树的展开状态, 下次打开或刷新时自动展开上次已展开的目录
- [重点] 保存子节点的展开状态, 下次展开当前节点, 会自动展开上次展开的子节点
- 自动定位当前文件所在的目录
- 移除 文档树的新建笔记按钮
- 移除 文档树和大纲的最小化按钮
效果
开发中
- 目前, 新建笔记本无法使用, 需要手动刷新页面
- 监听 "全部折叠按钮"
- 在全部折叠旁边: 增加全部展开按钮
- 在文档节点的按钮中, 增加全部展开按钮
- 增加支持 单击文档节点 展开的功能
实现原理:
- 记录已展开目录的 data-path, 笔记以 data-url 区分,以 object 嵌套的方式记录展开目录所在的层级关系
- 加载时,会根据记录的目录层级关系,依次展开
- 点击展开按钮,先从 BACK 里面查找是否有这个节点,如果有,就移动到 CFG 里面,然后展开
- 点击折叠按钮,将 CFG 的节点 移动到 BACK 里面
代码
/*******************************简介********************************************
# 文档树自动展开_release_v1.0
## 写在前面(重要!重要!重要!)
1. 抄的大佬的代码: https://ld246.com/article/1721771759337
2. 我只简单测了一下, 肯定有bug, 欢迎反馈 pc端的bug
3. 其他设备如果有问题, 请自行排查(我没需求), 手动狗头
4. 平时没啥时间, bug和开发中功能, 有时间再搞
5. 帖子不会更新了, 后续有更新会放到 回帖 里, 请随意食用
6. 实现原理比较简单粗暴, 如果文档比较多, 会有性能问题, 如果是性能问题, 请自行优化(我没需求)
## 当前功能
1. 记录文档树的展开状态, 下次打开或刷新时自动展开上次已展开的目录
2. [重点] 保存子节点的展开状态, 下次展开当前节点, 会自动展开上次展开的子节点
3. 自动定位当前文件所在的目录
4. 移除 文档树的新建笔记按钮
5. 移除 文档树和大纲的最小化按钮
## 开发中
1. 目前, 新建笔记本无法使用, 需要手动刷新页面
2. 监听 "全部折叠按钮"
3. 在全部折叠旁边: 增加全部展开按钮
4. 在文档节点的按钮中, 增加全部展开按钮
5. 增加支持 单击文档节点 展开的功能
## 实现原理:
1. 记录已展开目录的data-path, 笔记以data-url区分,以object嵌套的方式记录展开目录所在的层级关系
2. 加载时,会根据记录的目录层级关系,依次展开
3. 点击展开按钮,先从 BACK 里面查找是否有这个节点,如果有,就移动到 CFG 里面,然后展开
4. 点击折叠按钮,将 CFG 的节点 移动到 BACK 里面
*******************************************************************************/
/*******************************可自行修改的配置*********************************/
// 配置存储路径
const file_path = "/data/storage/tree_extend.json";
// 保存配置的时间间隔
const cfg_check_interval_t = 1 * (30*1000); // 30s检查一次
// 是否启用自动定位当前文件所在的目录
const is_show_file_position = false;
// 是否移除 文档树的新建笔记按钮
const remove_new_file_btn = false;
// 是否移除 文档树和大纲的最小化按钮
const remove_min_btn = false;
/*******************************************************************************/
/*******************************更新日志*****************************************
## 更新日志
**[增加/删除/修改/修复/优化/重构/文档/发布]**
---
### [2024/07/28_16:55:50] release_v1.0 基础版本
* [增加] 记录文档树的展开状态, 下次打开或刷新时自动展开上次已展开的目录
* [增加] 定位当前打开的文档
* [增加] 保存配置的时间间隔
* [增加] 移除 文档树的新建笔记按钮
* [增加] 移除 文档树和大纲的最小化按钮
*******************************************************************************/
/*******************************下面是实现代码**********************************/
const file_def_path = "/data/storage/tree_extend.json";
const CFG = "expand_element"
const BACK = "expand_back"
const VIS = "is_change"
const file_def_json = {
[CFG]: {},
[BACK]: {},
[VIS]: true
};
const file_def_info = JSON.stringify(file_def_json, null, 2);
const is_debug = false
const sbin_id = Date.now()
var main_cfg = file_def_json
function my_log(...args) {
if (is_debug) {
console.log(`[${sbin_id}]:`, ...args)
}
}
// 延迟执行
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 功能: 监听直到 元素存在
// 接受一个选择器参数(可以是字符串也可以是函数)
function when_element_exist(selector) {
// 返回一个 Promise 对象
return new Promise(resolve => {
// 定义一个内部函数 checkForElement 来检测是否存在特定元素
const checkForElement = () => {
// 初始化 isExist 变量为 false
let isExist = false;
// 如果传入的选择器是函数
if (typeof selector === 'function') {
// 执行选择器函数,获取返回值
isExist = selector();
} else {
// 如果选择器不是函数,则使用 document.querySelector 获取元素
isExist = document.querySelector(selector);
}
// 如果元素存在
if (isExist) {
// 解决 Promise,表示元素存在
resolve(true);
} else {
// 如果元素不存在,通过 requestAnimationFrame 在下一个动画帧继续检查
requestAnimationFrame(checkForElement);
}
};
// 第一次调用 checkForElement 函数开始检查元素是否存在
checkForElement();
});
}
// dfs遍历配置, 展开所有
async function expand_element_dfs(parent_cfg, book_url, parent_url = "") {
if (!parent_cfg) {
return;
}
my_log("auto parent_cfg:", parent_cfg)
// 遍历 parent_cfg 下面的节点, 并展开
for (let node_name in parent_cfg) {
if (!(node_name && parent_cfg[node_name])) {
// 异常判断, 跳过
continue;
}
// 拼接出节点路径 和 文档路径
let node_path = ""
let el_path = ""
if (parent_url == "") {
node_path = "/"
el_path = "/"
} else if (parent_url == "/") {
node_path = `${parent_url}${node_name}`;
el_path = `${node_path}.sy`;
} else {
node_path = `${parent_url}/${node_name}`;
el_path = `${node_path}.sy`;
}
// 找到折叠按钮
my_log("auto parent_url:", parent_url, "node_name:", node_name)
my_log("auto click node data-url:", book_url, "data-path:", el_path)
let arrow_btn = document.querySelector("ul.b3-list[data-url='" + book_url + "'] li[data-path='" + el_path + "'] span.b3-list-item__toggle");
if (!arrow_btn) {
// 如果没有找到,说明这个元素还没有渲染出来,等待一下
await sleep(10);
}
arrow_btn = document.querySelector("ul.b3-list[data-url='" + book_url + "'] li[data-path='" + el_path + "'] span.b3-list-item__toggle");
if (!arrow_btn) {
// 如果还是没有找到,说明异常了, 跳过
my_log("arrow_btn is nil, continue")
continue
}
my_log("auto find arrow_btn", arrow_btn)
// 判断是否已经展开
if (!arrow_btn.firstElementChild.classList.contains("b3-list-item__arrow--open")) {
my_log("auto click arrow_btn")
// 之前是折叠状态, 要展开
arrow_btn.click();
// 等待子元素渲染完毕
await sleep(10);
await when_element_exist(()=>{
// 监听, 直到找到 折叠元素的父级的兄弟节点 是ul
// 监听, 直到找到 这个文档树节点已经展开
return arrow_btn.closest("li").nextElementSibling?.tagName === 'UL';
});
}
// 递归展开配置里 当前节点的子节点
if (parent_cfg[node_name] !== null && typeof parent_cfg[node_name] === 'object' && !Array.isArray(parent_cfg[node_name])) {
my_log("auto dfs:", node_name)
await expand_element_dfs(parent_cfg[node_name], book_url, node_path);
}
}
}
// 获取文件内容
async function get_file_data() {
let path = file_path || file_def_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);
});
}
// 加载配置, 保证 返回的json里面一定有 CFG 和 BACK
async function get_expand_cfg_from_file() {
// 获取json配置里面的内容
let data = await get_file_data()
// 转换成json
if (data) {
data = JSON.parse(data);
}
if (!data) {
data = file_def_json;
}
if(data.code === 404) data = {};
if (!data[CFG] ||
!data[BACK]) {
data = file_def_json
}
data[VIS] = true
return data;
}
// 初始化配置, 在 get_expand_cfg_from_file 的基础上, 保证 每个笔记本的目录都有
function init_expand_cfg(book_url) {
if (!main_cfg[CFG][book_url]) {
main_cfg[CFG][book_url] = {};
}
if (!main_cfg[BACK][book_url]) {
main_cfg[BACK][book_url] = {};
}
}
// 写入文件内容
async function set_file_data(content) {
let path = file_path || file_def_path
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) {
my_log("File saved successfully");
}
else {
throw new Error("Failed to save file");
}
})
.catch((error) => {
console.error(error);
});
}
// 保存配置
function save_expand_cfg_to_file(data) {
// my_log(JSON.stringify(data, null, 2))
// 格式化json再保存, 方便查看
if (is_debug) {
set_file_data(JSON.stringify(data, null, 2))
} else {
set_file_data(JSON.stringify(data))
}
}
// 根据 path_arr 在 parent_obj 里面创建对象
function create_expand_obj_dfs(path_arr, parent_obj) {
if (path_arr.length === 0) return parent_obj; // 如果数组为空,退出递归
const node_name = path_arr.shift(); // 取出数组的第一个元素
if (!parent_obj[node_name]) {
parent_obj[node_name] = {}; // 创建一个新的空对象
}
return create_expand_obj_dfs(path_arr, parent_obj[node_name]); // 递归调用,传入新的路径数组和当前节点的子对象
}
// 用户点击折叠按钮, 修改并保存配置
async function change_and_save_expand_cfg(book, real_cb, arrow_btn, isOpen) {
const book_url = book.getAttribute("data-url");
// 找到arrowBtn父级的父级, 那里面有路径
const path_str = arrow_btn?.parentElement?.parentElement?.getAttribute("data-path");
// 将路径转换为数组, 删掉 ".sy" 结尾的字符, 按照 "/" 分割, 将路径转换成数组
const path_arr = path_str === "/" ? ["/"] : path_str.replace(/.sy$/i, '').split("/");
path_arr[0] = path_arr[0] === "" ? "/" : path_arr[0];
const path_arr_n = path_arr.length;
if (path_arr_n == 0 ) {
my_log("path_arr is empty")
return
}
if (!main_cfg[CFG][book_url]) {
main_cfg[CFG][book_url] = {};
}
if (!main_cfg[BACK][book_url]) {
main_cfg[BACK][book_url] = {};
}
// 找到父级节点(如果原来不存在, 就创建)
const node_name = path_arr.pop();
const parent_obj = create_expand_obj_dfs(path_arr, main_cfg[CFG][book_url]);
main_cfg[VIS] = true
my_log("tree node:", node_name, ", convert:", isOpen)
if (isOpen) {
// 需要展开
// 理论上: 父级节点里面是没有 node_name 的, 如果有, 说明异常, 但是不能影响正常展开
if (parent_obj[node_name]) {
my_log("move error: ", parent_obj[node_name])
}
// 如果是展开, 先去 BACK 里面查一下是否有这个节点
my_log("back cfg:", main_cfg[BACK][book_url])
if (main_cfg[BACK][book_url][node_name]) {
my_log("expand tree node, move BACK to CFG")
// 如果有, 就把 BACK 里面的节点 移动 到 CFG 里面
parent_obj[node_name] = main_cfg[BACK][book_url][node_name]
delete main_cfg[BACK][book_url][node_name];
// 递归展开 节点的子节点
my_log("user click before, removeEventListener click event")
book.removeEventListener("click", real_cb);
expand_element_dfs(parent_obj[node_name], book_url, path_str.replace(/.sy$/i, '')).then( () => {
my_log("user click after, addEventListener click event")
book.addEventListener("click", real_cb);
})
} else {
if (typeof parent_obj[node_name] === 'object' && Object.keys(parent_obj[node_name]).length > 0) {
// 走到这里, 说明是已经展开过的节点, 直接展开
my_log("user click before, removeEventListener click event")
book.removeEventListener("click", real_cb);
expand_element_dfs(parent_obj[node_name], book_url, path_str.replace(/.sy$/i, '')).then( () => {
my_log("user click after, addEventListener click event")
book.addEventListener("click", real_cb);
})
} else {
// 如果没有, 说明是新节点, 需要创建
parent_obj[node_name] = {};
}
}
} else {
my_log("fold tree node, move cfg to BACK")
// 如果是折叠, 需要把 CFG 里面的节点, 移走
if (typeof parent_obj[node_name] === 'object' && Object.keys(parent_obj[node_name]).length > 0) {
// 如果不是叶子节点, 要保存到 BACK 里面: 将 CFG 的节点 移动到 BACK 里面
main_cfg[BACK][book_url][node_name] = parent_obj[node_name];
}
delete parent_obj[node_name]
}
}
// 移除按钮
async function remove_btn() {
// 移除 新建笔记按钮
if (remove_new_file_btn) {
document.querySelectorAll('[data-type="new"]').forEach(async element => element.remove())
}
// 移除 最小化按钮
if (remove_min_btn) {
document.querySelectorAll('[data-type="min"]').forEach(async element => element.remove())
}
}
// 用户点击按钮的回调, 判断是否需要处理 折叠/展开
function click_arrow_btn_cb(book){
async function click_arrow_btn_cb_real(event) {
my_log("user click event:", book)
//等待箭头按钮渲染完成
// await sleep(40);
let arrow_btn = event.target;
if (arrow_btn.tagName == "LI") {
arrow_btn = arrow_btn.firstElementChild?.firstElementChild
} else if (arrow_btn.tagName == "SPAN") {
if (arrow_btn.childElementCount == 0) {
// 没有子元素, 说明点的是文档
if (arrow_btn.parentElement.getAttribute("data-path") == "/") {
// 路径是 / 说明是最外层的笔记本, 要处理, 其他不处理
arrow_btn = arrow_btn.parentElement.firstElementChild;
}
}
arrow_btn = arrow_btn.firstElementChild
} else if (arrow_btn.tagName == "use") {
// 点击的元素可能是折叠按钮的子元素
arrow_btn = arrow_btn.parentElement;
}
my_log("user click btn info", arrow_btn)
// 如果是箭头按钮
if (arrow_btn && arrow_btn.tagName == "svg" && arrow_btn.classList.contains("b3-list-item__arrow")) {
my_log("user click arrow btn")
//等待箭头按钮切换完成
const currentClassList = arrow_btn.classList.toString();
await when_element_exist(()=>{
return arrow_btn.classList.toString() !== currentClassList;
});
const isOpen = arrow_btn.classList.contains("b3-list-item__arrow--open");
// 存储当前元素的折叠状态
await change_and_save_expand_cfg(book, click_arrow_btn_cb_real, arrow_btn, isOpen);
}
remove_btn()
}
return click_arrow_btn_cb_real;
}
// 处理文档树的展开状态
async function monitor_tree_status() {
// 笔记列表渲染完成后, 触发
when_element_exist("ul.b3-list[data-url]").then(async myElement => {
// 防止更多笔记列表尚未渲染完成(笔记目录只渲染第一级,一般20毫秒足够了)
await sleep(20);
my_log("monitor_tree_status begin")
// 搜索笔记本
document.querySelectorAll("ul.b3-list[data-url]").forEach(async book => {
// 监听每个笔记本的折叠事件
// 当前笔记的 data-url
const book_url = book.getAttribute("data-url");
// 在cfg里面添加笔记本的url
init_expand_cfg(book_url)
// dfs遍历配置, 展开配置里面的节点
expand_element_dfs(main_cfg[CFG][book_url], book_url, "").then( () => {
// 展开之后, 监听这个笔记本的展开/折叠事件
//todo 新建笔记本无法被监听, 也就无法使用
my_log("init addEventListener click event:", book)
const clickHandler = click_arrow_btn_cb(book)
book.addEventListener("click", clickHandler);
});
});
// 移除按钮
remove_btn()
// 设置定时器
setInterval(async () => {
my_log("check cfg is change")
save_expand_cfg_to_file(main_cfg);
// 保存配置
if (!main_cfg.hasOwnProperty(VIS) || main_cfg[VIS] == true) {
main_cfg[VIS] = false
}
}, cfg_check_interval_t);
// 定位当前打开的文档
if(is_show_file_position){
await sleep(40);
document.querySelector(".layout-tab-container .block__icons span[data-type=focus]")?.click();
}
});
}
//todo 监听全部折叠按钮
async function monitor_fold_all_tree() {
const fold_all_btn_selector = "#layouts > div.fn__flex.fn__flex-1 > div.fn__flex-column.fn__flex-shrink.layout__dockl > div:nth-child(1) > div > div.layout-tab-container.fn__flex-1 > div > div.block__icons > span:nth-child(5)"
when_element_exist(fold_all_btn_selector).then(async () => {
const collapse_ele = document.querySelector(fold_all_btn_selector)
collapse_ele.addEventListener("click", async event => {
//todo
})
})
}
// 先加载配置
get_expand_cfg_from_file().then(async cfg => {
my_log("script begin")
main_cfg = cfg
// 处理文档树的展开状态
monitor_tree_status()
// 监听 全部折叠按钮
monitor_fold_all_tree()
});
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于