Skip to content

Commit fd7e559

Browse files
committed
Fix TaskNotes widgets in embedded task sections
1 parent b3aad15 commit fd7e559

5 files changed

Lines changed: 166 additions & 10 deletions

File tree

docs/releases/unreleased.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Example:
4040

4141
## Fixed
4242

43+
- (#1786) Fixed TaskNotes task cards and relationships/subtasks widgets appearing inside embedded task-note heading or block sections.
44+
- Skips note-level widget injection in detached or embedded Markdown editor contexts used by plugins such as Block Link Plus.
45+
- Thanks to @3zra47 for reporting.
4346
- (#1815) Fixed the Priority property settings so the NLP trigger character field is visible and editable even when priority NLP parsing is disabled.
4447
- Thanks to @spiv for reporting.
4548
- (#1035) Fixed the task modal title field causing the modal to jump to the bottom when focused on iPhone.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { EditorView } from "@codemirror/view";
2+
import { WorkspaceLeaf, editorInfoField } from "obsidian";
3+
4+
const EMBEDDED_MARKDOWN_CONTEXT_SELECTOR = [
5+
".blp-inline-edit-root",
6+
".internal-embed.markdown-embed",
7+
".markdown-embed",
8+
".popover.hover-popover",
9+
].join(", ");
10+
11+
function isDetachedLeaf(leaf: unknown): boolean {
12+
if (!leaf || typeof leaf !== "object") {
13+
return false;
14+
}
15+
16+
const leafRecord = leaf as { parent?: unknown };
17+
return "parent" in leafRecord && leafRecord.parent == null;
18+
}
19+
20+
function isEmbeddedMarkdownElement(element: HTMLElement | null | undefined): boolean {
21+
return Boolean(element?.closest(EMBEDDED_MARKDOWN_CONTEXT_SELECTOR));
22+
}
23+
24+
export function shouldSkipMarkdownWidgetEditor(view: EditorView): boolean {
25+
if (isEmbeddedMarkdownElement(view.dom)) {
26+
return true;
27+
}
28+
29+
try {
30+
const editorInfo = view.state.field(editorInfoField, false) as
31+
| {
32+
leaf?: WorkspaceLeaf;
33+
containerEl?: HTMLElement;
34+
}
35+
| undefined;
36+
37+
if (isDetachedLeaf(editorInfo?.leaf)) {
38+
return true;
39+
}
40+
41+
return isEmbeddedMarkdownElement(editorInfo?.containerEl);
42+
} catch (error) {
43+
console.debug("[TaskNotes] Error checking markdown widget editor context:", error);
44+
return false;
45+
}
46+
}
47+
48+
export function shouldSkipMarkdownWidgetLeaf(leaf: WorkspaceLeaf): boolean {
49+
if (isDetachedLeaf(leaf)) {
50+
return true;
51+
}
52+
53+
const viewContainer = (leaf.view as { containerEl?: HTMLElement } | undefined)?.containerEl;
54+
return isEmbeddedMarkdownElement(viewContainer);
55+
}

src/editor/RelationshipsDecorations.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ import {
6262
ReadingModeInjectionContext,
6363
ReadingModeInjectionScheduler,
6464
} from "./ReadingModeInjectionScheduler";
65+
import {
66+
shouldSkipMarkdownWidgetEditor,
67+
shouldSkipMarkdownWidgetLeaf,
68+
} from "./MarkdownWidgetContext";
6569

6670
// CSS class for identifying plugin-generated elements
6771
const CSS_RELATIONSHIPS_WIDGET = 'tasknotes-relationships-widget';
@@ -221,6 +225,8 @@ class RelationshipsDecorationsPlugin implements PluginValue {
221225
const editorElement = view.dom;
222226
if (!editorElement) return false;
223227

228+
if (shouldSkipMarkdownWidgetEditor(view)) return true;
229+
224230
const tableCell = editorElement.closest("td, th");
225231
if (tableCell) return true;
226232

@@ -303,15 +309,15 @@ class RelationshipsDecorationsPlugin implements PluginValue {
303309
// Remove any existing widget first
304310
this.removeWidget();
305311

312+
// Don't show note-level widgets in embedded or detached markdown editors
313+
if (this.isTableCellEditor(view)) {
314+
return;
315+
}
316+
306317
// Also clean up any orphaned widgets
307318
this.cleanupOrphanedWidgets(view);
308319

309320
try {
310-
// Don't show widget in table cell editors
311-
if (this.isTableCellEditor(view)) {
312-
return;
313-
}
314-
315321
// Check if relationships widget is enabled
316322
if (!this.plugin.settings.showRelationships) {
317323
return;
@@ -435,6 +441,10 @@ async function injectReadingModeWidget(
435441
return;
436442
}
437443

444+
if (shouldSkipMarkdownWidgetLeaf(leaf)) {
445+
return;
446+
}
447+
438448
const file = view.file;
439449
if (!file) {
440450
return;

src/editor/TaskCardNoteDecorations.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ import {
6868
ReadingModeInjectionContext,
6969
ReadingModeInjectionScheduler,
7070
} from "./ReadingModeInjectionScheduler";
71+
import {
72+
shouldSkipMarkdownWidgetEditor,
73+
shouldSkipMarkdownWidgetLeaf,
74+
} from "./MarkdownWidgetContext";
7175

7276
// CSS class for identifying plugin-generated elements
7377
const CSS_TASK_CARD_WIDGET = 'tasknotes-task-card-note-widget';
@@ -293,6 +297,8 @@ export class TaskCardNoteDecorationsPlugin implements PluginValue {
293297
const editorElement = view.dom;
294298
if (!editorElement) return false;
295299

300+
if (shouldSkipMarkdownWidgetEditor(view)) return true;
301+
296302
const tableCell = editorElement.closest("td, th");
297303
if (tableCell) return true;
298304

@@ -343,15 +349,15 @@ export class TaskCardNoteDecorationsPlugin implements PluginValue {
343349
// Remove any existing widget first
344350
this.removeWidget();
345351

352+
// Don't show note-level widgets in embedded or detached markdown editors
353+
if (this.isTableCellEditor(view)) {
354+
return;
355+
}
356+
346357
// Also clean up any orphaned widgets
347358
this.cleanupOrphanedWidgets(view);
348359

349360
try {
350-
// Don't show widget in table cell editors
351-
if (this.isTableCellEditor(view)) {
352-
return;
353-
}
354-
355361
// Check if task card widget is enabled
356362
if (!this.plugin.settings.showTaskCardInNote) {
357363
return;
@@ -427,6 +433,10 @@ async function injectReadingModeWidget(
427433
return;
428434
}
429435

436+
if (shouldSkipMarkdownWidgetLeaf(leaf)) {
437+
return;
438+
}
439+
430440
const file = view.file;
431441
if (!file) {
432442
return;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, expect, it, jest } from "@jest/globals";
2+
import { EditorView } from "@codemirror/view";
3+
import {
4+
shouldSkipMarkdownWidgetEditor,
5+
shouldSkipMarkdownWidgetLeaf,
6+
} from "../../../src/editor/MarkdownWidgetContext";
7+
8+
function createMockView(options: {
9+
dom?: HTMLElement;
10+
leaf?: { parent?: unknown };
11+
containerEl?: HTMLElement;
12+
}): EditorView {
13+
return {
14+
dom: options.dom ?? document.createElement("div"),
15+
state: {
16+
field: jest.fn(() => ({
17+
file: { path: "tasks/task.md" },
18+
leaf: options.leaf,
19+
containerEl: options.containerEl,
20+
})),
21+
},
22+
} as unknown as EditorView;
23+
}
24+
25+
describe("MarkdownWidgetContext", () => {
26+
it("skips detached editor leaves used by embedded inline editors", () => {
27+
const view = createMockView({
28+
leaf: { parent: null },
29+
});
30+
31+
expect(shouldSkipMarkdownWidgetEditor(view)).toBe(true);
32+
});
33+
34+
it("does not treat missing mock leaf parent data as detached", () => {
35+
const view = createMockView({
36+
leaf: {},
37+
});
38+
39+
expect(shouldSkipMarkdownWidgetEditor(view)).toBe(false);
40+
});
41+
42+
it("skips editors mounted inside markdown embeds", () => {
43+
const embed = document.createElement("div");
44+
embed.className = "internal-embed markdown-embed";
45+
const editor = document.createElement("div");
46+
embed.appendChild(editor);
47+
48+
const view = createMockView({
49+
dom: editor,
50+
leaf: { parent: {} },
51+
});
52+
53+
expect(shouldSkipMarkdownWidgetEditor(view)).toBe(true);
54+
});
55+
56+
it("skips Block Link Plus inline edit roots", () => {
57+
const root = document.createElement("div");
58+
root.className = "blp-inline-edit-root";
59+
const editor = document.createElement("div");
60+
root.appendChild(editor);
61+
62+
const view = createMockView({
63+
dom: editor,
64+
leaf: { parent: {} },
65+
});
66+
67+
expect(shouldSkipMarkdownWidgetEditor(view)).toBe(true);
68+
});
69+
70+
it("skips detached reading-mode leaves", () => {
71+
const leaf = {
72+
parent: undefined,
73+
view: { containerEl: document.createElement("div") },
74+
};
75+
76+
expect(shouldSkipMarkdownWidgetLeaf(leaf as any)).toBe(true);
77+
});
78+
});

0 commit comments

Comments
 (0)