[js] 求 JS 任务列表自动勾选 / 取消父任务

求一个功能,

一个目标列表中有多个子目标, 当子目标全部勾选后, 父目标自动勾选,

当子目标取消勾选后, 父目标也自动取消勾选

支持多层级目标列表, 即当三级目标列表完成后, 二级目标列表自动勾选, 当此二级目标列表为最后一个未勾选的, 这一级目标列表自动勾选

  • 思源笔记

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

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

    28443 引用 • 119762 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1984 回帖
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    11152 引用 • 50647 回帖 • 52 关注
1 操作
wilsons 在 2025-08-16 11:15:34 更新了该帖

相关帖子

被采纳的回答
  • wilsons 2 2 赞同

    试试这个

    r173.gif

    /*
    // 自动勾选取消父任务
    see https://ld246.com/article/1755052956165
    version 0.0.2
    0.0.2 增加新需求
    需求:
    一个任务列表中有多个子任务, 当子任务全部勾选后, 父任务自动勾选,
    当子任务有一个取消勾选后, 父任务也自动取消勾选
    支持多层级任务列表, 比如,当三级任务列表都勾选后, 二级任务列表自动勾选,
    当此二级任务列表为最后一个未勾选的, 那么这第一级任务列表也自动勾选
    
    新增需求:
    1. 当一个已完成的父任务新增一个子任务时,由于新子任务默认是未完成(未勾选),所以父任务应自动取消勾选。
    2. 当父任务下只剩一个未完成子任务时,若删除该子任务,则父任务应自动变为已完成(勾选)。
    */
    setTimeout(() => {
        const container = document.querySelector('.layout__center, #editor');
        if (!container) return;
        container.addEventListener('click', async (e) => {
            if (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || !e.target.closest('.protyle-action--task')) return;
            await new Promise(resolve => setTimeout(resolve, 50));
            // 获取祖先元素并模拟点击
            const item = e.target.closest('[data-subtype="t"][data-type="NodeListItem"]');
            clickParentItems(item);
        }, true);
    
        // 观察 container 的子树变化(基于规律的处理,可能有bug)
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                // 处理新增的子任务
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;
                    if (node.matches('div[data-type="NodeListItem"][data-subtype="t"]')) {
                        clickParentItems(node);
                    }
                    else if (node.matches('div[data-type="NodeList"][data-subtype="t"]')) {
                        const items = node.querySelectorAll('div[data-type="NodeListItem"][data-subtype="t"]');
                        items.forEach(item => clickParentItems(item));
                    } else {
                        const items = node.querySelectorAll('div[data-type="NodeListItem"][data-subtype="t"]');
                        if (items.length) items.forEach(item => clickParentItems(item));
                    }
                }
                // 处理删除的子任务
                for (const node of mutation.removedNodes) {
                    if (node.nodeType !== 1) continue;
                    if (node.matches('div[data-type="NodeListItem"][data-subtype="t"]')) {
                        clickParentItems(mutation.target.querySelector('div[data-type="NodeListItem"][data-subtype="t"]'));
                    }
                }
            }
        });
        observer.observe(container, { childList: true, subtree: true, attributes: false });
    
        function clickParentItems(item) {
            const parentItems = getParentsItems(item);
            if (parentItems.length === 0) return;
            for (const item of parentItems) {
                const list = item?.closest('[data-type="NodeList"][data-subtype="t"]');
                const hasUnDone = list?.querySelector(':scope > [data-subtype="t"][data-type="NodeListItem"] > .protyle-action--task use[*|href="#iconUncheck"]');
                const parentItem = item.parentElement?.closest('[data-type="NodeListItem"][data-subtype="t"]');
                const parentTaskCheck = parentItem?.querySelector('.protyle-action--task');
                const parentIsChecked = parentTaskCheck?.querySelector('use[*|href="#iconUncheck"]') ? false : true;
                // 当存在不可勾选的任务项目时,终止,不再向上级遍历,如果你想继续遍历可把此处的return改为continue
                if (!parentTaskCheck) return;
                if (hasUnDone) {
                    // 当有未完成的子任务时,如果父任务已勾选需求要取消
                    if (parentIsChecked) parentTaskCheck.click();
                } else {
                    // 当全部勾选了子任务时,如果父任务未勾选则勾选
                    if (!parentIsChecked) parentTaskCheck.click();
                }
            }
        }
    
        // 获取祖先任务项
        function getParentsItems(item, includeSelf = true) {
            if (!item) return [];
            const items = [];
            // 是否包含自己
            if (includeSelf) {
                items.push(item);
            }
            // 向上遍历所有符合条件的祖先 NodeListItem
            let current = item;
            while (true) {
                const parentItem = current.parentElement?.closest('[data-subtype="t"][data-type="NodeListItem"]');
                if (!parentItem) break;
                items.push(parentItem);
                current = parentItem;
            }
            return items;
        }
    }, 2000);
    

    题外话:

    今天发生了一件奇怪的事,我写代码时看了下时间 6:08,写完,又看了下时间还是 6:08,秒没注意。

    难道刚刚时间静止了?之前也出现过这种错觉。

    好吧,如果以后写代码时都能时间静止就好了,就可以无敌了trollface


    后人有诗叹曰trollface

    屏光冷映键飞忙,
    回看时针竟未航。
    若得封时锁宙宇,
    Deadline 前自徜徉。

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • wilsons 2 2 赞同

    试试这个

    r173.gif

    /*
    // 自动勾选取消父任务
    see https://ld246.com/article/1755052956165
    version 0.0.2
    0.0.2 增加新需求
    需求:
    一个任务列表中有多个子任务, 当子任务全部勾选后, 父任务自动勾选,
    当子任务有一个取消勾选后, 父任务也自动取消勾选
    支持多层级任务列表, 比如,当三级任务列表都勾选后, 二级任务列表自动勾选,
    当此二级任务列表为最后一个未勾选的, 那么这第一级任务列表也自动勾选
    
    新增需求:
    1. 当一个已完成的父任务新增一个子任务时,由于新子任务默认是未完成(未勾选),所以父任务应自动取消勾选。
    2. 当父任务下只剩一个未完成子任务时,若删除该子任务,则父任务应自动变为已完成(勾选)。
    */
    setTimeout(() => {
        const container = document.querySelector('.layout__center, #editor');
        if (!container) return;
        container.addEventListener('click', async (e) => {
            if (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || !e.target.closest('.protyle-action--task')) return;
            await new Promise(resolve => setTimeout(resolve, 50));
            // 获取祖先元素并模拟点击
            const item = e.target.closest('[data-subtype="t"][data-type="NodeListItem"]');
            clickParentItems(item);
        }, true);
    
        // 观察 container 的子树变化(基于规律的处理,可能有bug)
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                // 处理新增的子任务
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;
                    if (node.matches('div[data-type="NodeListItem"][data-subtype="t"]')) {
                        clickParentItems(node);
                    }
                    else if (node.matches('div[data-type="NodeList"][data-subtype="t"]')) {
                        const items = node.querySelectorAll('div[data-type="NodeListItem"][data-subtype="t"]');
                        items.forEach(item => clickParentItems(item));
                    } else {
                        const items = node.querySelectorAll('div[data-type="NodeListItem"][data-subtype="t"]');
                        if (items.length) items.forEach(item => clickParentItems(item));
                    }
                }
                // 处理删除的子任务
                for (const node of mutation.removedNodes) {
                    if (node.nodeType !== 1) continue;
                    if (node.matches('div[data-type="NodeListItem"][data-subtype="t"]')) {
                        clickParentItems(mutation.target.querySelector('div[data-type="NodeListItem"][data-subtype="t"]'));
                    }
                }
            }
        });
        observer.observe(container, { childList: true, subtree: true, attributes: false });
    
        function clickParentItems(item) {
            const parentItems = getParentsItems(item);
            if (parentItems.length === 0) return;
            for (const item of parentItems) {
                const list = item?.closest('[data-type="NodeList"][data-subtype="t"]');
                const hasUnDone = list?.querySelector(':scope > [data-subtype="t"][data-type="NodeListItem"] > .protyle-action--task use[*|href="#iconUncheck"]');
                const parentItem = item.parentElement?.closest('[data-type="NodeListItem"][data-subtype="t"]');
                const parentTaskCheck = parentItem?.querySelector('.protyle-action--task');
                const parentIsChecked = parentTaskCheck?.querySelector('use[*|href="#iconUncheck"]') ? false : true;
                // 当存在不可勾选的任务项目时,终止,不再向上级遍历,如果你想继续遍历可把此处的return改为continue
                if (!parentTaskCheck) return;
                if (hasUnDone) {
                    // 当有未完成的子任务时,如果父任务已勾选需求要取消
                    if (parentIsChecked) parentTaskCheck.click();
                } else {
                    // 当全部勾选了子任务时,如果父任务未勾选则勾选
                    if (!parentIsChecked) parentTaskCheck.click();
                }
            }
        }
    
        // 获取祖先任务项
        function getParentsItems(item, includeSelf = true) {
            if (!item) return [];
            const items = [];
            // 是否包含自己
            if (includeSelf) {
                items.push(item);
            }
            // 向上遍历所有符合条件的祖先 NodeListItem
            let current = item;
            while (true) {
                const parentItem = current.parentElement?.closest('[data-subtype="t"][data-type="NodeListItem"]');
                if (!parentItem) break;
                items.push(parentItem);
                current = parentItem;
            }
            return items;
        }
    }, 2000);
    

    题外话:

    今天发生了一件奇怪的事,我写代码时看了下时间 6:08,写完,又看了下时间还是 6:08,秒没注意。

    难道刚刚时间静止了?之前也出现过这种错觉。

    好吧,如果以后写代码时都能时间静止就好了,就可以无敌了trollface


    后人有诗叹曰trollface

    屏光冷映键飞忙,
    回看时针竟未航。
    若得封时锁宙宇,
    Deadline 前自徜徉。

    2 回复
    2 操作
    wilsons 在 2025-08-18 00:41:25 更新了该回帖
    wilsons 在 2025-08-16 11:57:23 更新了该回帖
  • Fighter93

    这段代码太有用了,非常感谢。

  • Muu

    可能需要点补充的功能,

    1. 当一个已完成字母表组新增一个子目标的时候, 相当于将增加了一个未完成的子目标, 那么上级目标列表应为未勾选状态
    2. 当只有一个子目标未完成时, 删除掉这个子目标, 那么上级列表应为勾选状态
    1 回复
  • Fighter93 1

    这段代码的解决方式是监听任务框的点击。而你说的这两点不需要点击任务框,要实现的话,需要建立更多监听,消耗更多的运行资源,最终实现的效果可能不会很好。

  • wilsons 1 1 赞同

    正如楼上 @Fighter93 大佬所说,这两个新增需求,让复杂度陡然上升。

    列表结构本身就复杂,再加上动态增加,删除,粘贴,移动等操作,让复杂度陡增。

    以下代码基于规律的实现,进行了简单测试,复杂时可能会有 bug

    根据自己的使用场景测试无误后使用,由此产生的任何后果,作者不承担任何责任。

    0.0.2 版更新内容如下:

    新增需求:

    1. 当一个已完成的父任务新增一个子任务时,由于新子任务默认是未完成(未勾选),所以父任务应自动取消勾选。
    2. 当父任务下只剩一个未完成子任务时,若删除该子任务,则父任务应自动变为已完成(勾选)。

    代码已在之前的帖子中更新

    还有一种实现方式,暴力法,即只要任务结点发生变化,均扫描一遍任务项进行判断,但这种方法,也有缺陷,即当用户手动取消勾选后,当别的任务修改时,可能会根据规律又自动勾选。由于这种方式实现简单,可借助 ai 实现,我测试时,已分别用 deepseek 和 chatgpt 成功实现,如需要这种的,请自行咨询 ai。

推荐标签 标签

  • Access
    1 引用 • 3 回帖 • 13 关注
  • jsoup

    jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

    6 引用 • 1 回帖 • 517 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    29 引用 • 202 回帖 • 54 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    9 引用 • 75 回帖 • 1 关注
  • OpenStack

    OpenStack 是一个云操作系统,通过数据中心可控制大型的计算、存储、网络等资源池。所有的管理通过前端界面管理员就可以完成,同样也可以通过 Web 接口让最终用户部署资源。

    10 引用 • 9 关注
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 191 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 9 关注
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 259 关注
  • Excel
    32 引用 • 29 回帖 • 1 关注
  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    316 引用 • 547 回帖 • 4 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    16 引用 • 143 回帖 • 6 关注
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    11 引用 • 15 回帖 • 1 关注
  • 知乎

    知乎是网络问答社区,连接各行各业的用户。用户分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

    10 引用 • 66 回帖
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    269 引用 • 666 回帖 • 1 关注
  • Solo

    Solo 是一款小而美的开源博客系统,专为程序员设计。Solo 有着非常活跃的社区,可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动(具体细节请浏览 B3log 构思 - 分布式社区网络)。

    这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!

    1449 引用 • 10092 回帖 • 489 关注
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 75 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖 • 1 关注
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 87 关注
  • Telegram

    Telegram 是一个非盈利性、基于云端的即时消息服务。它提供了支持各大操作系统平台的开源的客户端,也提供了很多强大的 APIs 给开发者创建自己的客户端和机器人。

    5 引用 • 35 回帖
  • 千千插件

    千千块(自定义块 css 和 js)
    可以用 ai 提示词来无限创作思源笔记

    32 引用 • 69 回帖 • 1 关注
  • 京东

    京东是中国最大的自营式电商企业,2015 年第一季度在中国自营式 B2C 电商市场的占有率为 56.3%。2014 年 5 月,京东在美国纳斯达克证券交易所正式挂牌上市(股票代码:JD),是中国第一个成功赴美上市的大型综合型电商平台,与腾讯、百度等中国互联网巨头共同跻身全球前十大互联网公司排行榜。

    14 引用 • 102 回帖 • 261 关注
  • Notion

    Notion - The all-in-one workspace for your notes, tasks, wikis, and databases.

    10 引用 • 80 回帖 • 1 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 687 关注
  • OkHttp

    OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。

    16 引用 • 6 回帖 • 99 关注
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    6 引用 • 144 回帖