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

当前效果

二级文档树.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);
})()

})()


  • 思源笔记

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

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

    20772 引用 • 81276 回帖

相关帖子

欢迎来到这里!

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

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

  • 初体验不错 👍

    两个建议:

    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
请输入回帖内容 ...