文档树自动展开_release_v1.0
写在前面(重要!重要!重要!)
- 抄的大佬的代码: https://ld246.com/article/1721771759337
- 我只简单测了一下, 肯定有 bug, 欢迎反馈 pc 端的 bug
- 其他设备如果有问题, 请自行排查(我没需求), 手动狗头
- 平时没啥时间, bug 和开发中功能, 有时间再搞
- 帖子不会更新了, 后续有更新会放到 回帖 里, 请随意食用
- 实现原理比较简单粗暴, 如果文档比较多, 会有性能问题, 如果是性能问题, 请自行优化(我没需求)
当前功能
- 记录文档树的展开状态, 下次打开或刷新时自动展开上次已展开的目录
- [重点] 保存子节点的展开状态, 下次展开当前节点, 会自动展开上次展开的子节点
- 自动定位当前文件所在的目录
- 移除 文档树的新建笔记按钮
- 移除 文档树和大纲的最小化按钮
效果

开发中
- 目前, 新建笔记本无法使用, 需要手动刷新页面
- 监听 "全部折叠按钮"
- 在全部折叠旁边: 增加全部展开按钮
- 在文档节点的按钮中, 增加全部展开按钮
- 增加支持 单击文档节点 展开的功能
实现原理:
- 记录已展开目录的 data-path, 笔记以 data-url 区分,以 object 嵌套的方式记录展开目录所在的层级关系
- 加载时,会根据记录的目录层级关系,依次展开
- 点击展开按钮,先从 BACK 里面查找是否有这个节点,如果有,就移动到 CFG 里面,然后展开
- 点击折叠按钮,将 CFG 的节点 移动到 BACK 里面
代码
const file_path = "/data/storage/tree_extend.json";
const cfg_check_interval_t = 1 * (30*1000);
const is_show_file_position = false;
const remove_new_file_btn = false;
const remove_min_btn = false;
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) {
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 expand_element_dfs(parent_cfg, book_url, parent_url = "") {
if (!parent_cfg) {
return;
}
my_log("auto 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(()=>{
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);
});
}
async function get_expand_cfg_from_file() {
let data = await get_file_data()
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;
}
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) {
if (is_debug) {
set_file_data(JSON.stringify(data, null, 2))
} else {
set_file_data(JSON.stringify(data))
}
}
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");
const path_str = arrow_btn?.parentElement?.parentElement?.getAttribute("data-path");
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) {
if (parent_obj[node_name]) {
my_log("move error: ", parent_obj[node_name])
}
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")
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")
if (typeof parent_obj[node_name] === 'object' && Object.keys(parent_obj[node_name]).length > 0) {
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)
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 => {
await sleep(20);
my_log("monitor_tree_status begin")
document.querySelectorAll("ul.b3-list[data-url]").forEach(async book => {
const book_url = book.getAttribute("data-url");
init_expand_cfg(book_url)
expand_element_dfs(main_cfg[CFG][book_url], book_url, "").then( () => {
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();
}
});
}
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 => {
})
})
}
get_expand_cfg_from_file().then(async cfg => {
my_log("script begin")
main_cfg = cfg
monitor_tree_status()
monitor_fold_all_tree()
});
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于