[代码片段分享] 二级文档树简略版 _release_v1.0

本贴最后更新于 213 天前,其中的信息可能已经时移世异

当前效果

二级文档树.gif

写在前面(重要!重要!重要!)

  1. 简单的实现, 效果不是很好, 但是没想到更好的交互, 暂时就这样, 如果你有更好的效果, 请联系我, 我会酌情考虑实现
  2. 我只简单测了一下, 肯定有 bug, 欢迎反馈 pc 端 的 bug, 备注: 其他设备如果有问题, 请自行排查(我没需求), 手动狗头
  3. 实现原理比较简单粗暴, 如果文档比较多, 会有性能问题, 如果是性能问题, 请自行优化(我没需求)
  4. 默认打印日志, 如果日志影响你了, 请自行将 is_debug 设置为 false

当前功能

  1. 单击文档树节点, 如果有子节点, 增加二级文档树页面显示子节点, 如果没有子节点, 会隐藏二级文档树页面
  2. 返回上级按钮

影响

  1. 二级文档树页面是直接复制的一级文档树元素, 大概率会对样式有影响
  2. 与单击节点展开效果上有冲突, 但不影响使用

实现原理:

以后再补吧(言外之意: 不会补了, 自己看代码吧)

代码

css 代码

css 代码也要添加, 不然鼠标悬浮不会置灰

.new_sy__file li:hover { background-color: var(--b3-list-hover); border-radius: var(--b3-border-radius); }

js 代码

/*******************************简介******************************************** # 二级文档树_release_v1.0 ## 写在前面(重要!重要!重要!) 1. 简单的实现, 效果不是很好, 但是没想到更好的交互, 暂时就这样, 如果你有更好的效果, 请联系我, 我会酌情考虑实现 2. 我只简单测了一下, 肯定有bug, 欢迎反馈 **pc端** 的bug, 备注: 其他设备如果有问题, 请自行排查(我没需求), 手动狗头 3. 实现原理比较简单粗暴, 如果文档比较多, 会有性能问题, 如果是性能问题, 请自行优化(我没需求) 4. 默认打印日志, 如果日志影响你了, 请自行将 `is_debug` 设置为 `false` ## 当前功能 1. 单击文档树节点, 如果有子节点, 增加二级文档树页面显示子节点, 如果没有子节点, 会隐藏二级文档树页面 2. 返回上级按钮 ## 影响 1. 二级文档树页面是直接复制的一级文档树元素, 大概率会对样式有影响 2. 与单击节点展开效果上有冲突, 但不影响使用 ## 实现原理: 以后再补吧(言外之意: 不会补了, 自己看代码吧) *******************************************************************************/ (async () => { let sub_tree_dock = null; let sub_resize = null; let li_template = null; let sub_tree_parent = null; let sub_tree_back = null; let sub_tree = null; let is_show = false; let click_book_path = null; let click_parent_path = null; let is_debug = true; const sbin_id = Date.now() 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(); }); } function get_all_book_ele() { return Array.from(document.querySelectorAll("ul.b3-list[data-url]")); } function monitor_tree_node_click_event(real_func) { if (real_func == null) { return; } get_all_book_ele()?.forEach(book => { // 监听点击事件 //todo 新建笔记本无法被监听, 也就无法使用 my_log("init addEventListener click event:", book); const clickHandler = (function(book) { async function clickHandlerReal(event) { my_log("user click event:", book) real_func(event, book, clickHandlerReal); } return clickHandlerReal; })(book); book.addEventListener("click", clickHandler); }); } const tree_click_e = { invalid: 0, // 无效的点击 node : 1, // 点击节点 arrow : 2, // 点击折叠按钮 } function get_tag_name_from_btn(btn) { return btn?.tagName?.toLowerCase() || ""; } function get_click_btn_type(click_btn) { if (click_btn == null) { return tree_click_e.invalid; } let tagName = get_tag_name_from_btn(click_btn); if (tagName == "use") { click_btn = click_btn.parentElement; tagName = get_tag_name_from_btn(click_btn); } if (tagName == "svg") { click_btn = click_btn.parentElement; tagName = get_tag_name_from_btn(click_btn); } if (tagName == "ul") { return tree_click_e.invalid; } else if (tagName == "li" && click_btn.getAttribute("data-path") == "/") { return tree_click_e.arrow; } else if (tagName == "li") { return tree_click_e.node; } else if (tagName == "span") { if (click_btn.classList.contains("b3-list-item__toggle")) { return tree_click_e.arrow; } else if (click_btn.classList.contains("b3-list-item__text") && click_btn.parentElement.getAttribute("data-path") == "/") { return tree_click_e.arrow; } else if (click_btn.classList.contains("b3-list-item__text")) { return tree_click_e.node; } else { return tree_click_e.invalid; } } return tree_click_e.invalid; } function is_click_tree_node(click_btn) { return (get_click_btn_type(click_btn) == tree_click_e.node); } function is_click_invalid_node(click_btn) { return (get_click_btn_type(click_btn) == tree_click_e.invalid); } function is_click_valid_node(click_btn) { return (!is_click_invalid_node(click_btn)); } const tree_node_deep_e = { invalid: 0, // li : 1, // span : 2, // svg : 3, // use : 4, // } function get_deep_level_from_name(name) { return tree_node_deep_e[name] || tree_node_deep_e.invalid; } function get_deep_level_from_btn(btn) { let tagName = get_tag_name_from_btn(btn); return get_deep_level_from_name(tagName); } function get_ele_from_click_btn(click_btn, type_name) { if (click_btn == null) { return null; } let click_deep = get_deep_level_from_btn(click_btn); let ret_deep = get_deep_level_from_name(type_name); while (click_deep < ret_deep) { click_btn = click_btn?.firstElementChild; click_deep++; } while (click_deep > ret_deep) { click_btn = click_btn?.parentElement; click_deep--; } if (click_deep == ret_deep) { return click_btn; } return null; } async function get_tree_li_from_path(book_url, el_path) { if (book_url == null || book_url == "" || el_path == null || el_path == "") { return null; } const selector_str = `ul.b3-list[data-url='${book_url}'] li[data-path='${el_path}']`; await when_element_exist(selector_str); return document.querySelector(selector_str); } // 折叠按钮是否是展开的 function is_open_arrow_btn(span_btn_arrow) { return span_btn_arrow?.firstElementChild?.classList?.contains("b3-list-item__arrow--open") } // 折叠按钮是否是折叠的 function is_close_arrow_btn(span_btn_arrow) { return !is_open_arrow_btn(span_btn_arrow) } // 通过路径, 找到折叠按钮 async function get_arrow_btn_from_path(book_url, el_path) { if (book_url == null || book_url == "" || el_path == null || el_path == "") { return null; } const selector_str = `ul.b3-list[data-url='${book_url}'] li[data-path='${el_path}']>span.b3-list-item__toggle`; await when_element_exist(selector_str); return document.querySelector(selector_str); } // 通过路径, 展开折叠按钮 async function open_arrow_btn_from_path(book_url, el_path) { let arrow_span = await get_arrow_btn_from_path(book_url, el_path); if (is_close_arrow_btn(arrow_span)) { my_log("需要展开"); // 之前是折叠状态, 要展开 arrow_span.click(); } await when_element_exist(() => { // 监听, 直到找到 折叠元素的父级的兄弟节点 是ul // 监听, 直到找到 这个文档树节点已经展开 return arrow_span.closest("li").nextElementSibling?.tagName === 'UL'; }); } // 获取子节点内容 async function get_sub_tree(notebook, path) { return fetch("/api/filetree/listDocsByPath", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ notebook, 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_sub_tree_node_list_from_api(notebook, file_path) { if (notebook == null || file_path == null) { return null; } // 获取json配置里面的内容 let data = await get_sub_tree(notebook, file_path) // 转换成json if (data) { data = JSON.parse(data); } if(data.code == 0) { return data.data; } return null; } // 新增二级文档树页面 async function page_init() { // 找到文档树页面 await when_element_exist('div.layout__dockl') let src_tree_dock = document.querySelector('div.layout__dockl') let src_resize = src_tree_dock.nextElementSibling let src_li_ele = document.querySelector(".sy__file ul>li"); // 复制并插入元素 sub_tree_dock = src_tree_dock.cloneNode(true) sub_resize = src_resize.cloneNode(true) li_template = src_li_ele.cloneNode(true) src_tree_dock.insertAdjacentElement("afterend", sub_resize) src_resize.insertAdjacentElement("beforebegin", sub_tree_dock) // 有一些样式是根据.sy__file搜索的, 所以要换掉 sub_tree_parent = sub_tree_dock.querySelector(".sy__file") // sub_tree_parent.classList.remove('sy__file'); sub_tree_parent.classList.add('new_sy__file'); // 删掉一些没用的元素 Array.from(sub_tree_parent.children).forEach(child => { if (child.classList.contains('block__icons')) { // 处理头部 Array.from(child.children).forEach(sub_child => { if (sub_child.classList.contains('block__logo')) { sub_child.childNodes[2].textContent = '二级树' // 插入返回按钮 let newElementType = "sub_tree_back_btn" let displayContent = "返回上一级" sub_child.insertAdjacentHTML("afterend", `<span data-type="${newElementType}" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="${displayContent}"> <svg><use xlink:href="#iconBack" style="opacity: 1;"></use></svg> </span>`); // 事件监听 let back_ele = child.querySelector(`[data-type="${newElementType}"]`); back_ele?.addEventListener("click", sub_tree_back_click_handler); } else { sub_child.remove() } }) } else if (child.classList.contains('fn__flex-1')) { // 处理树 sub_tree_back = child.cloneNode(true); child.remove() Array.from(sub_tree_back.children).forEach(sub_child => { sub_child.remove() }) } else { child.remove() } }) sub_tree_parent.addEventListener("click", sub_tree_node_click_handler); my_log(sub_tree_parent) // 处理二级文档树节点, 删掉没用的内容 Array.from(li_template.children).forEach(child_ele => { if (child_ele.classList.contains("b3-list-item__toggle")) { child_ele.firstElementChild?.classList.remove("b3-list-item__arrow--open") } if (!(child_ele.classList.contains("b3-list-item__toggle") || child_ele.classList.contains("b3-list-item__text"))) { child_ele.remove(); } }) is_show = true; } function hide_sub_tree_node() { if (!is_show) { return; // 原本就是隐藏的话, 直接退出 } sub_tree_dock.remove(); sub_tree_dock = null; sub_resize.remove(); sub_resize = null; li_template = null; sub_tree_parent = null; sub_tree_back = null; sub_tree = null; is_show = false; click_book_path = null; click_parent_path = null; } function clear_sub_tree_node() { sub_tree?.remove() sub_tree = sub_tree_back.cloneNode(true) sub_tree_parent.appendChild(sub_tree); } async function show_sub_tree_node(notebook, parent_path, node_list) { if (!is_show) { // 之前是隐藏的, 需要初始化 await page_init(); } // 清空文档树 clear_sub_tree_node(); click_book_path = notebook; click_parent_path = parent_path; // 遍历子节点, 并在二级文档树里面显示 node_list.forEach(node => { let new_li = li_template.cloneNode(true); sub_tree.appendChild(new_li) new_li.lastElementChild.textContent = node.name?.slice(0, -3); new_li.setAttribute('data-path', node.path); if (node.subFileCount == 0) { new_li.firstElementChild.classList.add('fn__hidden'); } }) } // 点击一级文档树 async function src_tree_node_click_handler(event, book, clickHandler){ let click_btn = event.target; if (is_click_invalid_node(click_btn)) { // 不是点击叶子节点的跳过 return; } my_log("点击一级文档树") // 找到路径 let notebook = book?.getAttribute("data-url"); let parent_path = get_ele_from_click_btn(click_btn, "li")?.getAttribute("data-path"); // 获取子节点列表 let node_list_data = await get_sub_tree_node_list_from_api(notebook, parent_path) let node_list = node_list_data?.files; // 没有子节点, 隐藏二级文档树 if (node_list == null || node_list.length == 0) { hide_sub_tree_node() return; } // 有子节点, 有显示二级文档树 show_sub_tree_node(notebook, parent_path, node_list) } // 点击二级文档树 async function sub_tree_node_click_handler(event) { let click_btn = event.target; if (is_click_invalid_node(click_btn)) { // 不是点击叶子节点的跳过 return; } my_log("点击二级文档树") // 展开父节点 open_arrow_btn_from_path(click_book_path, click_parent_path); // 找到子节点并点击 let click_node_path = get_ele_from_click_btn(click_btn, "li")?.getAttribute("data-path"); let click_node_src = await get_tree_li_from_path(click_book_path, click_node_path); click_node_src?.click(); // 定位当前打开的文档 await sleep(40); document.querySelector(".layout-tab-container .block__icons span[data-type=focus]").click(); } // 点击返回上级按钮 async function sub_tree_back_click_handler(event) { my_log("点击返回上级按钮") // 找到父节点 let click_node_src = await get_tree_li_from_path(click_book_path, click_parent_path); click_node_src?.parentElement?.previousElementSibling?.click(); // 定位当前打开的文档 await sleep(40); document.querySelector(".layout-tab-container .block__icons span[data-type=focus]")?.click(); } // 主流程 (async () => { // 监听一级文档树点击事件 await when_element_exist("ul.b3-list[data-url]") monitor_tree_node_click_event(src_tree_node_click_handler); })() })()
  • 思源笔记

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

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

    24827 引用 • 102133 回帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • 👍 先点赞,慢慢看

  • 初体验不错 👍

    两个建议:

    1. 点击子二级树下的末级文档,二级树不消失,换句话说,永远不自动消失,用户点击关闭才消失。
    2. 二级树显示期间,一级树的末级文档没必要显示了。
    1 回复
  • EmberSky
    1. 点击末级文档不消失 这个确实是要搞的

    这是后来才发现的, 涉及到关闭和打开按钮放在哪里, 框架也不太一样, 就没改

    1. 一级树原有的功能, 我不会改, 就保留原有的效果了
    1 回复
    1. 位置我觉得可以放到这里,一个开关按钮即可

      image.png

    2. 一级树的末级文档隐藏我觉得可以用选择符 li:has(span.b3-list-item__toggle.fn__hidden) 匹配,意思是只匹配那些隐藏了展开折叠箭头的 li,即末级文档了,本来可以用 li[data-count="0"]来匹配的,但官方这个文档数有 bug,新建的目录 data-count 也显示 0,这就很混乱了。

      但这个一级树的文档是动态展示的,要每次目录点击后都要处理一下其下的子文档,建议用 css 全部隐藏更方便些。

    3. 如果隐藏末级文档,要考虑新建,删除,移动等操作怎么处理?文档复制到二级树,脱离了原来 dom 后是否可行未可知,可以试试看。不过这样就变得麻烦了。

    1 回复
  • EmberSky

    开关可以搞, 有空我加一下

    1 回复
  • 哈哈哈,不过有个思路不知是否可行,你试试看。

    就是一级树末级文档不是被隐藏了后,然后在二级树文档里右键的时候,让它直接触发原一级树下的相同菜单,然后菜单位置稍微改一改,挪动到二级树那里即可。

    或者二级树点击末级文档时,原一级树下的父目录别展开,不然,如果文档多了,一级树会变得很长,还得滚动,和不使用二级树差不多了,就失去了意义。

    1 回复
  • EmberSky 1 赞同
    1. 我不会改菜单位置
    2. 不展开就不能点击这个文档了, 不过可以展开之后再折叠
    1 回复
  • 昨天还发现了 2 个小问题,1 是显示二级树时导致一级树不能左右拖拽宽度了,2 是二级树的类名 layout__dockl 和一级树的一样,这在依赖这个类名的插件或样式时可能出现意外。

    1 回复
  • EmberSky
    1. 修改宽度没做, 不会 😂, 现在可以关掉后调整, 再打开
    2. 类名也是因为不会, 没找到重命名的方案, 所以是直接复制的, 这个在帖子的影响里面有说
  • Floria233 1 评论

    2024 年 8 月 30 日 11:17:28

    • 根据楼下发帖总结,本层是关于文档树功能的反馈楼
    • 希望大家可以在这层接力【评论】,尽量避免重新发帖回复,这样会导致整个对话界面非常长,信息容易丢失。
    • 如果有文档树功能的需求楼,请另发贴(依旧请盖楼评论,发帖会造成非常长的瀑布界面)

    目前问题汇总:

    最重要的一条(我个人觉得)——
    二级树只处于预览状态,还未能实现最重要的“拖拽”功能。

    1. 点击子二级树下的末级文档,二级树会消失——
      改善:希望二级树不要自动消失,或者用户点击关闭才消失。
    2. 二级树显示期间,一级树的末级文档仍然在开启状态,一级树和二级树信息产生重合。
    3. 二级树开启时,一级树不能调整宽度。
    4. 二级树的类名 layout__dockl 和一级树的一样,这在依赖这个类名的插件或样式时可能出现意外。(这个好专业啊,我只是个粘贴工。希望 @wilsons 说直白点就好啦哈哈)
    @Floria233 4 解释下,类名类似一个人的家庭住址,通过这个地址可以找到你,这往往是插件或样式用来定位页面元素的,一般类名可以多个元素共享,就像你和家庭成员共享一个地址一样,但某些特殊的类通常是全局唯一的,比如这个 layout_dockl,往往只是标注左侧面板的,但如果其他面板也占用了这个类名,就可能导致一些冲突问题。 @EmberSky 我一般复制后改下类名,然后把这个类名相关的 css 也复制一遍(不求完全一样,差不多就可以了),不过也可以通过 getComputedStyle 获取样式后,然后把目标按照获取的值设置一遍,理论上可以,我没有试过 ^_^
    wilsons
  • wenbocn

    期待可以实现 OneNote 的目录样式。

    1. 一级目录是 docker 上的一个按钮,点击后可以选择不同笔记本。
    2. 二级目录横向排列,显示在顶端。
    3. 三级、四级目录显示在文档树中。
  • EmberSky 2

    @Floria233 二级文档树有插件了, 我这边之后就不会再更新 js 代码了
    这个插件已经实现了大部分功能

    image.png

    2 回复
  • Floria233

    感谢大佬,刚刚试用,功能方面确实没啥问题,不过有个设计我很迷惑,就是——文档树的“气泡”问题。

    如图,鼠标放到二级文档树的标题上时,随着我的鼠标飞舞不停出现的气泡,这个东西,以后可以更新关掉吗?

    原因是我之前发过的帖子 😂 文档树的气泡提示实在太烦人了,强烈建议更改! - 链滴 (ld246.com)

    之前是在一级文档树上有这个问题,靠着“文档树自定义”这个插件解决了,现在二级文档树上这个问题如出一辙啊 😂

    无论如何,再度非常感谢大佬出手,执行力太强了。

    1.PNG

    另,我换了一个库,立刻发现一个新问题——如果一个文件夹内的子文档太多,二级文档树的界面切换就会失效,即点击一级文档树 A 下的 A1 文件夹到 B1 文件夹,二级文档树没有反应,界面没有变化。这个与点击频率似乎没有太大关系,只要是文档层级过多,切换就会 失效。

    1 回复
    1 操作
    Floria233 在 2024-09-20 15:17:22 更新了该回帖
  • 为何没有动态显示和隐藏二级文档树的开关?即当打开开关时才开启二级文档树。现在想关闭二级文档树时需要去插件里关闭,很不方便。

    1 回复
  • EmberSky

    呃, 别骂了, 别骂了, 这不是我做的插件, 我也是今天逛集市的时候无意中看到的

    1 回复
  • EmberSky 1 评论

    夸奖我就收下了, 但是声明一下, 这插件不是我做的, 哈哈哈

    O(∩_∩)O 哈哈~我昨天实际还在内心疑惑了一下,为什么大佬你上插件市场要改名……总之,还是感谢通知。
    Floria233
  • wilsons 1 评论
    可恶,网络又抽风了,github 上不去,我又没有梯子 😂 在这里感谢一下那位大佬深藏功与名,等网络好了再去看看
    Floria233
请输入回帖内容 ...