Skip to content

Commit dbd8bda

Browse files
authored
Merge pull request #3338 from ilandikov/fix-list-item-checkboxes
fix: Toggle non-task checkbox lines in tree view
2 parents ba94268 + 2d4486d commit dbd8bda

File tree

10 files changed

+111
-25
lines changed

10 files changed

+111
-25
lines changed

docs/Queries/Layout.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ The `show tree` instruction enables us to see the parent/child relationships in
137137
138138
- For now, **all child tasks and list items are displayed**, regardless of whether they match the query.
139139
- In the screenshot above, `Decide who to invite` did not match the `not done` query, but it is still shown.
140-
- If you are using the Global Filter, any child tasks without the Global Filter are rendered without their checkbox. We are tracking this in [issue #3170](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/3170).
141140
- Any **sorting instructions only affect the sorting of the left-most tasks** in the results list.
142141
- Child tasks and list items are displayed in the order that they appear in the file. They are not affected by any `sort by` instructions.
143142
- For now, the **tree layout is turned off by default**, whilst we explore how it should interact with the filtering instructions.

resources/sample_vaults/Tasks-Demo/Manual Testing/Smoke Testing the Tasks Plugin.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,24 @@ hide postpone button
187187

188188
---
189189

190+
### Toggling non-task checkboxes
191+
192+
- Non-task checkbox are tasks without the global filter, treated as `ListItems` by the obsidian-tasks plugin
193+
- [ ] #task Check or uncheck non-task list items with checkbox in the query below this task
194+
- [ ] I will have an `x` status
195+
- [x] I will have a `space` status
196+
- [/] Me too with a `space` status
197+
198+
```tasks
199+
filename includes {{query.file.filename}}
200+
heading includes Toggling non-task checkboxes
201+
show tree
202+
```
203+
204+
- [ ] #task **check**: Checked all above steps for **toggling non-task list items with checkbox in a query** worked
205+
206+
---
207+
190208
## Check the plugin starts OK with no `data.json` settings file
191209

192210
- Preparation

resources/sample_vaults/Tasks-Demo/Test Data/inheritance_non_task_child.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
- [ ] #task task parent
22
- [ ] #task task child
33
- [ ] non-task child
4+
- [x] non-task child status x
45
- list item child
56

67
```tasks

src/Obsidian/File.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ListItemCache, MetadataCache, Notice, TFile, Vault, Workspace } from 'obsidian';
22
import { GlobalFilter } from '../Config/GlobalFilter';
33
import { type MockListItemCache, type MockTask, saveMockDataForTesting } from '../lib/MockDataCreator';
4+
import type { ListItem } from '../Task/ListItem';
45
import type { Task } from '../Task/Task';
56
import { logging } from '../lib/logging';
67
import { logEndOfTaskEdit, logStartOfTaskEdit } from '../lib/LogTasksHelper';
@@ -48,8 +49,8 @@ export const replaceTaskWithTasks = async ({
4849
originalTask,
4950
newTasks,
5051
}: {
51-
originalTask: Task;
52-
newTasks: Task | Task[];
52+
originalTask: ListItem;
53+
newTasks: ListItem | ListItem[];
5354
}): Promise<void> => {
5455
if (vault === undefined || metadataCache === undefined || workspace === undefined) {
5556
errorAndNotice('Tasks: cannot use File before initializing it.');
@@ -113,8 +114,8 @@ const tryRepetitive = async ({
113114
workspace,
114115
previousTries,
115116
}: {
116-
originalTask: Task;
117-
newTasks: Task[];
117+
originalTask: ListItem;
118+
newTasks: ListItem[];
118119
vault: Vault;
119120
metadataCache: MetadataCache;
120121
workspace: Workspace;
@@ -163,7 +164,7 @@ Recommendations:
163164
// Finally, we can insert 1 or more lines over the original task line:
164165
const updatedFileLines = [
165166
...fileLines.slice(0, taskLineNumber),
166-
...newTasks.map((task: Task) => task.toFileLineString()),
167+
...newTasks.map((task: ListItem) => task.toFileLineString()),
167168
...fileLines.slice(taskLineNumber + 1), // Only supports single-line tasks.
168169
];
169170

@@ -188,7 +189,7 @@ Recommendations:
188189
* It may throw a WarningWorthRetrying exception in several cases that justify a retry (should be handled by the caller)
189190
* or an Error exception in case of an unrecoverable error.
190191
*/
191-
async function getTaskAndFileLines(task: Task, vault: Vault): Promise<[number, TFile, string[]]> {
192+
async function getTaskAndFileLines(task: ListItem, vault: Vault): Promise<[number, TFile, string[]]> {
192193
if (metadataCache === undefined) throw new WarningWorthRetrying();
193194
// Validate our inputs.
194195
// For permanent failures, return nothing.

src/Renderer/QueryResultsRenderer.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { IQuery } from '../IQuery';
66
import { QueryLayout } from '../Layout/QueryLayout';
77
import { TaskLayout } from '../Layout/TaskLayout';
88
import { PerformanceTracker } from '../lib/PerformanceTracker';
9+
import { replaceTaskWithTasks } from '../Obsidian/File';
910
import { explainResults, getQueryForQueryRenderer } from '../Query/QueryRendererHelper';
1011
import { State } from '../Obsidian/Cache';
1112
import type { GroupDisplayHeading } from '../Query/Group/GroupDisplayHeading';
@@ -339,12 +340,43 @@ export class QueryResultsRenderer {
339340
return await this.addTask(taskList, taskLineRenderer, listItem, taskIndex, queryRendererParameters);
340341
}
341342

342-
return await this.addListItem(taskList, listItem);
343+
return await this.addListItem(taskList, listItem, taskIndex);
343344
}
344345

345-
private async addListItem(taskList: HTMLUListElement, listItem: ListItem) {
346+
private async addListItem(taskList: HTMLUListElement, listItem: ListItem, listItemIndex: number) {
346347
const li = createAndAppendElement('li', taskList);
347348

349+
if (listItem.statusCharacter) {
350+
const checkbox = createAndAppendElement('input', li);
351+
checkbox.classList.add('task-list-item-checkbox');
352+
checkbox.type = 'checkbox';
353+
354+
checkbox.addEventListener('click', (event: MouseEvent) => {
355+
event.preventDefault();
356+
// It is required to stop propagation so that obsidian won't write the file with the
357+
// checkbox (un)checked. Obsidian would write after us and overwrite our change.
358+
event.stopPropagation();
359+
360+
// Should be re-rendered as enabled after update in file.
361+
checkbox.disabled = true;
362+
363+
const checkedOrUncheckedListItem = listItem.checkOrUncheck();
364+
replaceTaskWithTasks({ originalTask: listItem, newTasks: checkedOrUncheckedListItem });
365+
});
366+
367+
if (listItem.statusCharacter !== ' ') {
368+
checkbox.checked = true;
369+
li.classList.add('is-checked');
370+
}
371+
372+
li.classList.add('task-list-item');
373+
374+
// Set these to be compatible with stock obsidian lists:
375+
li.setAttribute('data-task', listItem.statusCharacter.trim());
376+
// Trim to ensure empty attribute for space. Same way as obsidian.
377+
li.setAttribute('data-line', listItemIndex.toString());
378+
}
379+
348380
const span = createAndAppendElement('span', li);
349381
await this.textRenderer(
350382
listItem.description,
@@ -353,6 +385,15 @@ export class QueryResultsRenderer {
353385
this.obsidianComponent,
354386
);
355387

388+
// Unwrap the p-tag that was created by the MarkdownRenderer:
389+
const pElement = span.querySelector('p');
390+
if (pElement !== null) {
391+
while (pElement.firstChild) {
392+
span.insertBefore(pElement.firstChild, pElement);
393+
}
394+
pElement.remove();
395+
}
396+
356397
return li;
357398
}
358399

src/lib/LogTasksHelper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Task } from '../Task/Task';
1+
import type { ListItem } from '../Task/ListItem';
22
import type { Logger } from './logging';
33

44
/**
@@ -7,7 +7,7 @@ import type { Logger } from './logging';
77
* @param codeLocation - a string description, such as 'callingFunctionName()'.
88
* @param originalTask
99
*/
10-
export function logStartOfTaskEdit(logger: Logger, codeLocation: string, originalTask: Task) {
10+
export function logStartOfTaskEdit(logger: Logger, codeLocation: string, originalTask: ListItem) {
1111
logger.debug(
1212
`${codeLocation}: task line number: ${originalTask.taskLocation.lineNumber}. file path: "${originalTask.path}"`,
1313
);
@@ -20,8 +20,8 @@ export function logStartOfTaskEdit(logger: Logger, codeLocation: string, origina
2020
* @param codeLocation - a string description, such as 'callingFunctionName()'.
2121
* @param newTasks
2222
*/
23-
export function logEndOfTaskEdit(logger: Logger, codeLocation: string, newTasks: Task[]) {
24-
newTasks.map((task: Task, index: number) => {
23+
export function logEndOfTaskEdit(logger: Logger, codeLocation: string, newTasks: ListItem[]) {
24+
newTasks.map((task: ListItem, index: number) => {
2525
// Alignment of task lines is intentionally consistent between logStartOfTaskEdit() and this:
2626
logger.debug(`${codeLocation} ==> ${index + 1} : ${task.toFileLineString()}`);
2727
});

src/lib/MockDataCreator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ListItemCache, Pos } from 'obsidian';
2+
import type { ListItem } from '../Task/ListItem';
23
import type { Task } from '../Task/Task';
34

45
// See File.test.ts for how to use this.
@@ -44,7 +45,7 @@ export type MockTogglingDataForTesting = {
4445
* @see saveMockDataForTesting
4546
*/
4647
export function getMockDataForTesting(
47-
originalTask: Task,
48+
originalTask: ListItem,
4849
fileLines: string[],
4950
listItemsCache: ListItemCache[],
5051
): MockTogglingDataForTesting {
@@ -86,7 +87,7 @@ export function getMockDataForTesting(
8687
* @param fileLines
8788
* @param listItemsCache
8889
*/
89-
export function saveMockDataForTesting(originalTask: Task, fileLines: string[], listItemsCache: ListItemCache[]) {
90+
export function saveMockDataForTesting(originalTask: ListItem, fileLines: string[], listItemsCache: ListItemCache[]) {
9091
const everything = getMockDataForTesting(originalTask, fileLines, listItemsCache);
9192
console.error(`Inconsistent lines: SAVE THE OUTPUT
9293
data:

tests/Obsidian/Cache.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ describe('cache', () => {
495495
"- [ ] #task task parent
496496
- [ ] #task task child
497497
- [ ] non-task child
498+
- [x] non-task child status x
498499
- list item child
499500
500501
\`\`\`tasks
@@ -510,6 +511,7 @@ describe('cache', () => {
510511
"- [ ] #task task parent : Task
511512
- [ ] #task task child : Task
512513
- [ ] non-task child : ListItem
514+
- [x] non-task child status x : ListItem
513515
- list item child : ListItem
514516
"
515517
`);

tests/Obsidian/__test_data__/inheritance_non_task_child.json

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,31 @@
5353
"parent": 0,
5454
"position": {
5555
"end": {
56-
"col": 21,
56+
"col": 33,
5757
"line": 3,
58-
"offset": 98
58+
"offset": 110
5959
},
6060
"start": {
6161
"col": 3,
6262
"line": 3,
6363
"offset": 80
6464
}
65+
},
66+
"task": "x"
67+
},
68+
{
69+
"parent": 0,
70+
"position": {
71+
"end": {
72+
"col": 21,
73+
"line": 4,
74+
"offset": 132
75+
},
76+
"start": {
77+
"col": 3,
78+
"line": 4,
79+
"offset": 114
80+
}
6581
}
6682
}
6783
],
@@ -70,8 +86,8 @@
7086
"position": {
7187
"end": {
7288
"col": 21,
73-
"line": 3,
74-
"offset": 98
89+
"line": 4,
90+
"offset": 132
7591
},
7692
"start": {
7793
"col": 0,
@@ -85,13 +101,13 @@
85101
"position": {
86102
"end": {
87103
"col": 3,
88-
"line": 8,
89-
"offset": 164
104+
"line": 9,
105+
"offset": 198
90106
},
91107
"start": {
92108
"col": 0,
93-
"line": 5,
94-
"offset": 100
109+
"line": 6,
110+
"offset": 134
95111
}
96112
},
97113
"type": "code"
@@ -130,7 +146,7 @@
130146
}
131147
]
132148
},
133-
"fileContents": "- [ ] #task task parent\n - [ ] #task task child\n - [ ] non-task child\n - list item child\n\n```tasks\nfilename includes {{query.file.filename}}\nshow tree\n```\n",
149+
"fileContents": "- [ ] #task task parent\n - [ ] #task task child\n - [ ] non-task child\n - [x] non-task child status x\n - list item child\n\n```tasks\nfilename includes {{query.file.filename}}\nshow tree\n```\n",
134150
"filePath": "Test Data/inheritance_non_task_child.md",
135151
"getAllTags": [
136152
"#task",

tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_should_render_non_task_check_box_when_global_filter_is_enabled.approved.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,14 @@
4545
<a class="tasks-edit" title="Edit task" href="#"></a>
4646
</span>
4747
</li>
48-
<li><span>non-task child</span></li>
48+
<li class="task-list-item" data-task="" data-line="1">
49+
<input class="task-list-item-checkbox" type="checkbox" />
50+
<span>non-task child</span>
51+
</li>
52+
<li class="is-checked task-list-item" data-task="x" data-line="2">
53+
<input class="task-list-item-checkbox" type="checkbox" />
54+
<span>non-task child status x</span>
55+
</li>
4956
<li><span>list item child</span></li>
5057
</ul>
5158
</li>

0 commit comments

Comments
 (0)