mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
editor: fix old inline images in bullet,numbered,task&check lists
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`check list item > inline image as first child in check list item 1`] = `"<ul class="simple-checklist"><li class="simple-checklist--item"><p data-spacing="double">item 1</p></li><li class="simple-checklist--item"><p data-spacing="double"></p><img src="image.png" data-aspect-ratio="1"></li></ul>"`;
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
createEditor,
|
||||
h,
|
||||
p,
|
||||
checkList,
|
||||
checkListItem
|
||||
} from "../../../../test-utils/index.js";
|
||||
import { CheckList } from "../../check-list/check-list.js";
|
||||
import { CheckListItem } from "../check-list-item.js";
|
||||
import { Paragraph } from "../../paragraph/paragraph.js";
|
||||
import { ImageNode } from "../../image/image.js";
|
||||
|
||||
describe("check list item", () => {
|
||||
/**
|
||||
* see https://github.com/streetwriters/notesnook/pull/8877 for more context
|
||||
*/
|
||||
test("inline image as first child in check list item", async () => {
|
||||
const el = checkList(
|
||||
checkListItem([p(["item 1"])]),
|
||||
checkListItem([h("img", [], { src: "image.png" })])
|
||||
);
|
||||
|
||||
const { editor } = createEditor({
|
||||
initialContent: el.outerHTML,
|
||||
extensions: {
|
||||
checkList: CheckList,
|
||||
checkListItem: CheckListItem.configure({ nested: true }),
|
||||
paragraph: Paragraph,
|
||||
image: ImageNode
|
||||
}
|
||||
});
|
||||
|
||||
expect(editor.getHTML()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@tiptap/core";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { CheckList } from "../check-list/check-list.js";
|
||||
import { ensureLeadingParagraph } from "../../utils/prosemirror.js";
|
||||
|
||||
export interface CheckListItemOptions {
|
||||
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
|
||||
@@ -67,7 +68,8 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
|
||||
return [
|
||||
{
|
||||
tag: `li.simple-checklist--item`,
|
||||
priority: 51
|
||||
priority: 51,
|
||||
getContent: ensureLeadingParagraph
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
@@ -18,8 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ListItem as TiptapListItem } from "@tiptap/extension-list-item";
|
||||
import { ensureLeadingParagraph } from "../../utils/prosemirror.js";
|
||||
|
||||
export const ListItem = TiptapListItem.extend({
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
priority: 100,
|
||||
tag: `li`,
|
||||
getContent: ensureLeadingParagraph
|
||||
}
|
||||
];
|
||||
},
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
|
||||
@@ -5,3 +5,5 @@ exports[`hitting backspace at the start of first list item 1`] = `"<div><div con
|
||||
exports[`hitting backspace at the start of the second (or next) list item 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><ul><li><p>item1item2</p></li></ul></div></div>"`;
|
||||
|
||||
exports[`hitting backspace at the start of the second (or next) paragraph inside the list item 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><ul><li><p>item 1item 2</p></li></ul></div></div>"`;
|
||||
|
||||
exports[`inline image as first child in list item 1`] = `"<ul><li><p data-spacing="double">item 1</p></li><li><p data-spacing="double"></p><img src="image.png" data-aspect-ratio="1"></li></ul>"`;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
@@ -66,17 +66,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
{
|
||||
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
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
@@ -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`] = `"<ul class="checklist"><li class="checklist--item"><p data-spacing="double">item 1</p></li><li class="checklist--item"><p data-spacing="double"></p><img src="image.png" data-aspect-ratio="1"></li></ul>"`;
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
@@ -17,52 +17,39 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 "
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user