求一个功能,
一个目标列表中有多个子目标, 当子目标全部勾选后, 父目标自动勾选,
当子目标取消勾选后, 父目标也自动取消勾选
支持多层级目标列表, 即当三级目标列表完成后, 二级目标列表自动勾选, 当此二级目标列表为最后一个未勾选的, 这一级目标列表自动勾选
求一个功能,
一个目标列表中有多个子目标, 当子目标全部勾选后, 父目标自动勾选,
当子目标取消勾选后, 父目标也自动取消勾选
支持多层级目标列表, 即当三级目标列表完成后, 二级目标列表自动勾选, 当此二级目标列表为最后一个未勾选的, 这一级目标列表自动勾选
思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。
融合块、大纲和双向链接,重构你的思维。
代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。
用户在该标签下分享代码片段时需在帖子标题前添加 [css] 或 [js] 用于区分代码片段类型。
提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。
试试这个

/*
// 自动勾选取消父任务
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,秒没注意。
难道刚刚时间静止了?之前也出现过这种错觉。
好吧,如果以后写代码时都能时间静止就好了,就可以无敌了
后人有诗叹曰
:
屏光冷映键飞忙,
回看时针竟未航。
若得封时锁宙宇,
Deadline 前自徜徉。
试试这个

/*
// 自动勾选取消父任务
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,秒没注意。
难道刚刚时间静止了?之前也出现过这种错觉。
好吧,如果以后写代码时都能时间静止就好了,就可以无敌了
后人有诗叹曰
:
屏光冷映键飞忙,
回看时针竟未航。
若得封时锁宙宇,
Deadline 前自徜徉。
正如楼上 @Fighter93 大佬所说,这两个新增需求,让复杂度陡然上升。
列表结构本身就复杂,再加上动态增加,删除,粘贴,移动等操作,让复杂度陡增。
以下代码基于规律的实现,进行了简单测试,复杂时可能会有 bug。
请根据自己的使用场景测试无误后使用,由此产生的任何后果,作者不承担任何责任。
0.0.2 版更新内容如下:
新增需求:
代码已在之前的帖子中更新。
还有一种实现方式,暴力法,即只要任务结点发生变化,均扫描一遍任务项进行判断,但这种方法,也有缺陷,即当用户手动取消勾选后,当别的任务修改时,可能会根据规律又自动勾选。由于这种方式实现简单,可借助 ai 实现,我测试时,已分别用 deepseek 和 chatgpt 成功实现,如需要这种的,请自行咨询 ai。
前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。
Thymeleaf 是一款用于渲染 XML/XHTML/HTML5 内容的模板引擎。类似 Velocity、 FreeMarker 等,它也可以轻易的与 Spring 等 Web 框架进行集成作为 Web 应用的模板引擎。与其它模板引擎相比,Thymeleaf 最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个 Web 应用。
Webswing 是一个能将任何 Swing 应用通过纯 HTML5 运行在浏览器中的 Web 服务器,详细介绍请看 将 Java Swing 应用变成 Web 应用 。
GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。
CAP 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。
webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。
Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。
Latke 是一款以 JSON 为主的 Java Web 框架。
Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。
Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。
Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。
链滴是一个记录生活的地方。
记录生活,连接点滴
Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。
Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。
单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。
DevOps(Development 和 Operations 的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。
Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。
Wide 是一款基于 Web 的 Go 语言 IDE。通过浏览器就可以进行 Go 开发,并有代码自动完成、查看表达式、编译反馈、Lint、实时结果输出等功能。
欢迎访问我们运维的实例: https://wide.b3log.org
Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。
Notion - The all-in-one workspace for your notes, tasks, wikis, and databases.
GitBook 使您的团队可以轻松编写和维护高质量的文档。 分享知识,提高团队的工作效率,让用户满意。