"`;
exports[`hitting backspace at the start of the second (or next) paragraph inside the list item 1`] = `"
"`;
+
+exports[`inline image as first child in list item 1`] = `"
item 1

"`;
diff --git a/packages/editor/src/extensions/list-item/tests/list-item.test.ts b/packages/editor/src/extensions/list-item/tests/list-item.test.ts
index acfbea17d..382d31d05 100644
--- a/packages/editor/src/extensions/list-item/tests/list-item.test.ts
+++ b/packages/editor/src/extensions/list-item/tests/list-item.test.ts
@@ -23,6 +23,8 @@ import { createEditor, h, li, p, ul } from "../../../../test-utils/index.js";
import BulletList from "../../bullet-list/index.js";
import OrderedList from "../../ordered-list/index.js";
import { ListItem } from "../index.js";
+import { Paragraph } from "../../paragraph/paragraph.js";
+import { ImageNode } from "../../image/image.js";
test("hitting backspace at the start of first list item", async () => {
const el = ul([li([p(["item1"])]), li([p(["item2"])])]);
@@ -97,3 +99,24 @@ test("hitting backspace at the start of the second (or next) paragraph inside th
await new Promise((resolve) => setTimeout(resolve, 100));
expect(editorElement.outerHTML).toMatchSnapshot();
});
+
+/**
+ * see https://github.com/streetwriters/notesnook/pull/8877 for more context
+ */
+test("inline image as first child in list item", async () => {
+ const el = ul([
+ li([p(["item 1"])]),
+ li([h("img", [], { src: "image.png" })])
+ ]);
+
+ const { editor } = createEditor({
+ initialContent: el.outerHTML,
+ extensions: {
+ listItem: ListItem,
+ paragraph: Paragraph,
+ image: ImageNode
+ }
+ });
+
+ expect(editor.getHTML()).toMatchSnapshot();
+});
diff --git a/packages/editor/src/extensions/outline-list-item/outline-list-item.ts b/packages/editor/src/extensions/outline-list-item/outline-list-item.ts
index 9b0801dfb..b26574b5b 100644
--- a/packages/editor/src/extensions/outline-list-item/outline-list-item.ts
+++ b/packages/editor/src/extensions/outline-list-item/outline-list-item.ts
@@ -24,12 +24,12 @@ import {
} from "@tiptap/core";
import {
findParentNodeOfTypeClosestToPos,
- isClickWithinBounds
+ isClickWithinBounds,
+ ensureLeadingParagraph
} from "../../utils/prosemirror.js";
import { OutlineList } from "../outline-list/outline-list.js";
import { keybindings, tiptapKeys } from "@notesnook/common";
import { Paragraph } from "../paragraph/paragraph.js";
-import { DOMParser } from "@tiptap/pm/model";
export interface ListItemOptions {
HTMLAttributes: Record
;
@@ -66,17 +66,7 @@ export const OutlineListItem = Node.create({
{
priority: 100,
tag: `li[data-type="${this.name}"]`,
- getContent: (node, schema) => {
- const parser = DOMParser.fromSchema(schema);
- const fragment = parser.parse(node).content;
- const firstNode = fragment.firstChild;
- if (firstNode && firstNode.type.name !== "paragraph") {
- const emptyParagraph = schema.nodes.paragraph.create();
- return fragment.addToStart(emptyParagraph);
- }
-
- return fragment;
- }
+ getContent: ensureLeadingParagraph
}
];
},
diff --git a/packages/editor/src/extensions/task-item/__tests__/__snapshots__/task-item.test.ts.snap b/packages/editor/src/extensions/task-item/__tests__/__snapshots__/task-item.test.ts.snap
new file mode 100644
index 000000000..df94e21a9
--- /dev/null
+++ b/packages/editor/src/extensions/task-item/__tests__/__snapshots__/task-item.test.ts.snap
@@ -0,0 +1,3 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`task list item > inline image as first child in task list item 1`] = `"item 1

"`;
diff --git a/packages/editor/src/extensions/task-item/__tests__/task-item.test.ts b/packages/editor/src/extensions/task-item/__tests__/task-item.test.ts
new file mode 100644
index 000000000..c9300c8ff
--- /dev/null
+++ b/packages/editor/src/extensions/task-item/__tests__/task-item.test.ts
@@ -0,0 +1,55 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { describe, expect, test } from "vitest";
+import {
+ createEditor,
+ h,
+ p,
+ taskList,
+ taskItem
+} from "../../../../test-utils/index.js";
+import { TaskListNode } from "../../task-list/task-list.js";
+import { TaskItemNode } from "../task-item.js";
+import { Paragraph } from "../../paragraph/paragraph.js";
+import { ImageNode } from "../../image/image.js";
+
+describe("task list item", () => {
+ /**
+ * see https://github.com/streetwriters/notesnook/pull/8877 for more context
+ */
+ test("inline image as first child in task list item", async () => {
+ const el = taskList(
+ taskItem([p(["item 1"])]),
+ taskItem([h("img", [], { src: "image.png" })])
+ );
+
+ const { editor } = createEditor({
+ initialContent: el.outerHTML,
+ extensions: {
+ taskList: TaskListNode,
+ taskListItem: TaskItemNode.configure({ nested: true }),
+ paragraph: Paragraph,
+ image: ImageNode
+ }
+ });
+
+ expect(editor.getHTML()).toMatchSnapshot();
+ });
+});
diff --git a/packages/editor/src/extensions/task-item/task-item.ts b/packages/editor/src/extensions/task-item/task-item.ts
index 467fea976..7f4af2db1 100644
--- a/packages/editor/src/extensions/task-item/task-item.ts
+++ b/packages/editor/src/extensions/task-item/task-item.ts
@@ -21,6 +21,7 @@ import { mergeAttributes } from "@tiptap/core";
import { TaskItem } from "@tiptap/extension-task-item";
import { TaskItemComponent } from "./component.js";
import { createNodeView } from "../react/index.js";
+import { ensureLeadingParagraph } from "../../utils/prosemirror.js";
export type TaskItemAttributes = {
checked: boolean;
@@ -56,7 +57,8 @@ export const TaskItemNode = TaskItem.extend({
return [
{
tag: ".checklist > li",
- priority: 51
+ priority: 100,
+ getContent: ensureLeadingParagraph
}
];
},
diff --git a/packages/editor/src/extensions/task-list/__tests__/task-list.test.ts b/packages/editor/src/extensions/task-list/__tests__/task-list.test.ts
index e1ea43854..826a73c9d 100644
--- a/packages/editor/src/extensions/task-list/__tests__/task-list.test.ts
+++ b/packages/editor/src/extensions/task-list/__tests__/task-list.test.ts
@@ -17,52 +17,39 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { createEditor, h, ul, li } from "../../../../test-utils/index.js";
+import {
+ createEditor,
+ taskItem,
+ taskList
+} from "../../../../test-utils/index.js";
import { test, expect } from "vitest";
import { TaskListNode } from "../index.js";
import { TaskItemNode } from "../../task-item/index.js";
import { p, eq } from "prosemirror-test-builder";
import { countCheckedItems, deleteCheckedItems, sortList } from "../utils.js";
-function taskList(...children: HTMLLIElement[]) {
- return ul(children, { class: "checklist" });
-}
-
-function taskItem(
- text: string,
- attr: { checked?: boolean } = {},
- subList?: HTMLUListElement
-) {
- const children: HTMLElement[] = [h("p", [text])];
- if (subList) children.push(subList);
-
- return li(children, {
- class: "checklist--item " + (attr.checked ? "checked" : "")
- });
-}
-
const NESTED_TASK_LIST = taskList(
- taskItem("Task item 1", { checked: true }),
- taskItem("Task item 2"),
+ taskItem(["Task item 1"], { checked: true }),
+ taskItem(["Task item 2"]),
taskItem(
- "Task item 3",
+ ["Task item 3"],
{ checked: false },
taskList(
- taskItem("Task item 4", { checked: true }),
- taskItem("Task item 5"),
+ taskItem(["Task item 4"], { checked: true }),
+ taskItem(["Task item 5"]),
taskItem(
- "Task item 6",
+ ["Task item 6"],
{ checked: false },
taskList(
- taskItem("Task item 7", { checked: true }),
- taskItem("Task item 8", { checked: true }),
+ taskItem(["Task item 7"], { checked: true }),
+ taskItem(["Task item 8"], { checked: true }),
taskItem(
- "Task item 9",
+ ["Task item 9"],
{ checked: false },
taskList(
- taskItem("Task item 10", { checked: true }),
- taskItem("Task item 11", { checked: true }),
- taskItem("Task item 12")
+ taskItem(["Task item 10"], { checked: true }),
+ taskItem(["Task item 11"], { checked: true }),
+ taskItem(["Task item 12"])
)
)
)
@@ -99,8 +86,8 @@ test(`count items in a task list`, async () => {
test(`delete checked items in a task list`, async () => {
const { editor } = createEditor({
initialContent: taskList(
- taskItem("Task item 1", { checked: true }),
- taskItem("Task item 2")
+ taskItem(["Task item 1"], { checked: true }),
+ taskItem(["Task item 2"])
).outerHTML,
extensions: {
taskItem: TaskItemNode.configure({ nested: true }),
@@ -132,15 +119,15 @@ test(`delete checked items in a nested task list`, async () => {
test(`delete checked items in a task list with no checked items should do nothing`, async () => {
const { editor } = createEditor({
initialContent: taskList(
- taskItem("Task item 1", { checked: false }),
- taskItem("Task item 2"),
+ taskItem(["Task item 1"], { checked: false }),
+ taskItem(["Task item 2"]),
taskItem(
- "Task item 3",
+ ["Task item 3"],
{ checked: false },
taskList(
- taskItem("Task item 4", { checked: false }),
- taskItem("Task item 5"),
- taskItem("Task item 6", { checked: false })
+ taskItem(["Task item 4"], { checked: false }),
+ taskItem(["Task item 5"]),
+ taskItem(["Task item 6"], { checked: false })
)
)
).outerHTML,
@@ -172,8 +159,10 @@ test(`sort checked items to the bottom of the task list`, async () => {
test(`sorting a task list with no checked items should do nothing`, async () => {
const { editor } = createEditor({
- initialContent: taskList(taskItem("Task item 1"), taskItem("Task item 2"))
- .outerHTML,
+ initialContent: taskList(
+ taskItem(["Task item 1"]),
+ taskItem(["Task item 2"])
+ ).outerHTML,
extensions: {
taskItem: TaskItemNode.configure({ nested: true }),
taskList: TaskListNode
diff --git a/packages/editor/src/utils/prosemirror.ts b/packages/editor/src/utils/prosemirror.ts
index f3f7c406d..ff2068dd7 100644
--- a/packages/editor/src/utils/prosemirror.ts
+++ b/packages/editor/src/utils/prosemirror.ts
@@ -31,7 +31,10 @@ import {
NodeType,
ResolvedPos,
Attrs,
- Slice
+ Slice,
+ DOMParser,
+ Schema,
+ Fragment
} from "prosemirror-model";
import { EditorState, Selection, Transaction } from "prosemirror-state";
import TextStyle from "@tiptap/extension-text-style";
@@ -394,3 +397,16 @@ export function isClickWithinBounds(
return false;
}
}
+
+export function ensureLeadingParagraph(node: Node, schema: Schema): Fragment {
+ const parser = DOMParser.fromSchema(schema);
+ const fragment = parser.parse(node).content;
+ const firstNode = fragment.firstChild;
+
+ if (firstNode && firstNode.type.name !== "paragraph") {
+ const emptyParagraph = schema.nodes.paragraph.create();
+ return fragment.addToStart(emptyParagraph);
+ }
+
+ return fragment;
+}
diff --git a/packages/editor/test-utils/index.ts b/packages/editor/test-utils/index.ts
index 3474c87de..40a3d3df0 100644
--- a/packages/editor/test-utils/index.ts
+++ b/packages/editor/test-utils/index.ts
@@ -108,3 +108,36 @@ export function outlineListItem(
"data-type": "outlineListItem"
});
}
+
+export function taskList(...children: HTMLLIElement[]) {
+ return ul(children, { class: "checklist" });
+}
+
+export function taskItem(
+ paragraphChildren: (string | HTMLElement)[],
+ attr: { checked?: boolean } = {},
+ subList?: HTMLUListElement
+) {
+ const children: HTMLElement[] = [h("p", paragraphChildren)];
+ if (subList) children.push(subList);
+
+ return li(children, {
+ class: "checklist--item " + (attr.checked ? "checked" : "")
+ });
+}
+
+export function checkList(...children: HTMLLIElement[]) {
+ return ul(children, { class: "simple-checklist" });
+}
+
+export function checkListItem(
+ paragraphChildren: (string | HTMLElement)[],
+ subList?: HTMLUListElement
+) {
+ const children: HTMLElement[] = [h("p", paragraphChildren)];
+ if (subList) children.push(subList);
+
+ return li(children, {
+ class: "simple-checklist--item "
+ });
+}