Merge pull request #8772 from 01zulfi/editor/fix-images-in-outline-lists

editor: fix inserting images in outline lists
This commit is contained in:
Abdullah Atta
2025-10-16 11:48:34 +05:00
committed by GitHub
8 changed files with 501 additions and 51 deletions

View File

@@ -0,0 +1,171 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`migration > inline image in outline list 1`] = `
{
"content": [
{
"content": [
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "item 1",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "hello",
"type": "text",
},
],
"type": "paragraph",
},
{
"attrs": {
"align": undefined,
"aspectRatio": 1,
"filename": undefined,
"hash": undefined,
"height": null,
"mime": undefined,
"progress": 0,
"size": undefined,
"src": "image.png",
"type": "image",
"width": null,
},
"type": "image",
},
{
"content": [
{
"text": "world",
"type": "text",
},
],
"type": "paragraph",
},
{
"content": [
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "sub item 2",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "sub item 3",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
],
"type": "outlineList",
},
],
"type": "outlineListItem",
},
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "item 4",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
],
"type": "outlineList",
},
],
"type": "doc",
}
`;
exports[`migration > inline image in paragraph 1`] = `
{
"content": [
{
"content": [
{
"text": "hello",
"type": "text",
},
],
"type": "paragraph",
},
{
"attrs": {
"align": undefined,
"aspectRatio": 1,
"filename": undefined,
"hash": undefined,
"height": null,
"mime": undefined,
"progress": 0,
"size": undefined,
"src": "image.png",
"type": "image",
"width": null,
},
"type": "image",
},
{
"content": [
{
"text": "world",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "doc",
}
`;

View File

@@ -0,0 +1,75 @@
/*
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 {
createEditor,
h,
p,
outlineList,
outlineListItem
} from "../../../../test-utils/index.js";
import { test, expect, describe } from "vitest";
import { ImageNode } from "../index.js";
import { OutlineList } from "../../outline-list/outline-list.js";
import { OutlineListItem } from "../../outline-list-item/outline-list-item.js";
describe("migration", () => {
test(`inline image in paragraph`, async () => {
const el = p(["hello", h("img", [], { src: "image.png" }), "world"]);
const {
builder: { image },
editor
} = createEditor({
initialContent: el.outerHTML,
extensions: {
image: ImageNode.configure({})
}
});
expect(editor.getJSON()).toMatchSnapshot();
});
test(`inline image in outline list`, async () => {
const el = outlineList(
outlineListItem(["item 1"]),
outlineListItem(
["hello", h("img", [], { src: "image.png" }), "world"],
outlineList(
outlineListItem(["sub item 2"]),
outlineListItem(["sub item 3"])
)
),
outlineListItem(["item 4"])
);
const {
builder: { image },
editor
} = createEditor({
initialContent: el.outerHTML,
extensions: {
outlineList: OutlineList,
outlineListItem: OutlineListItem,
image: ImageNode
}
});
expect(editor.getJSON()).toMatchSnapshot();
});
});

View File

@@ -131,39 +131,6 @@ export const ImageNode = Node.create<ImageOptions>({
getAttrs(node) {
if (node.querySelectorAll("img").length <= 0) return false;
return null;
},
getContent: (dom, schema) => {
const wrapper = document.createElement("div");
let buffer = "";
const flushBuffer = () => {
if (buffer.trim().length > 0) {
const pEl = document.createElement("p");
pEl.innerHTML = buffer;
wrapper.appendChild(pEl);
buffer = "";
}
};
for (const child of dom.childNodes) {
if (
child.nodeType === globalThis.Node.ELEMENT_NODE &&
(child as HTMLElement).tagName === "IMG"
) {
flushBuffer();
wrapper.appendChild(child);
} else {
if (child.nodeType === globalThis.Node.ELEMENT_NODE) {
buffer += (child as HTMLElement).outerHTML;
} else if (child.nodeType === globalThis.Node.TEXT_NODE) {
buffer += child.textContent;
}
}
}
flushBuffer();
const parser = DOMParser.fromSchema(schema);
return parser.parse(wrapper).content;
}
},
{

View File

@@ -0,0 +1,130 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`outline list item > code block in outline list item 1`] = `
{
"content": [
{
"content": [
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "item 1",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "hello",
"type": "text",
},
],
"type": "paragraph",
},
{
"attrs": {
"caretPosition": undefined,
"id": "codeblock-test-id-123456",
"indentLength": 2,
"indentType": "space",
"language": null,
"lines": [],
},
"content": [
{
"text": "const x = 1;",
"type": "text",
},
],
"type": "codeblock",
},
{
"content": [
{
"text": "world",
"type": "text",
},
],
"type": "paragraph",
},
{
"content": [
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "sub item 2",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "sub item 3",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
],
"type": "outlineList",
},
],
"type": "outlineListItem",
},
{
"attrs": {
"collapsed": false,
},
"content": [
{
"content": [
{
"text": "item 4",
"type": "text",
},
],
"type": "paragraph",
},
],
"type": "outlineListItem",
},
],
"type": "outlineList",
},
],
"type": "doc",
}
`;

View File

@@ -0,0 +1,73 @@
/*
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 {
createEditor,
h,
li,
outlineList,
outlineListItem
} from "../../../../test-utils/index.js";
import { test, expect, describe, beforeAll, vi } from "vitest";
import { OutlineList } from "../../outline-list/outline-list.js";
import { OutlineListItem } from "../outline-list-item.js";
import { CodeBlock } from "../../code-block/code-block.js";
describe("outline list item", () => {
beforeAll(() => {
vi.mock("nanoid", () => ({
nanoid: () => "test-id-123456"
}));
});
test(`code block in outline list item`, async () => {
const subList = outlineList(
outlineListItem(["sub item 2"]),
outlineListItem(["sub item 3"])
);
const listItemWithCodeBlock = li(
[
h("p", ["hello"]),
h("pre", [h("code", ["const x = 1;"])]),
h("p", ["world"]),
subList
],
{ "data-type": "outlineListItem" }
);
const el = outlineList(
outlineListItem(["item 1"]),
listItemWithCodeBlock,
outlineListItem(["item 4"])
);
const {
builder: { codeBlock },
editor
} = createEditor({
initialContent: el.outerHTML,
extensions: {
outlineList: OutlineList,
outlineListItem: OutlineListItem,
codeBlock: CodeBlock
}
});
expect(editor.getJSON()).toMatchSnapshot();
});
});

View File

@@ -25,6 +25,7 @@ import {
import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror.js";
import { OutlineList } from "../outline-list/outline-list.js";
import { keybindings, tiptapKeys } from "@notesnook/common";
import { Paragraph } from "../paragraph/paragraph.js";
export interface ListItemOptions {
HTMLAttributes: Record<string, unknown>;
@@ -52,13 +53,14 @@ export const OutlineListItem = Node.create<ListItemOptions>({
};
},
content: "paragraph+ list?",
content: "block+",
defining: true,
parseHTML() {
return [
{
priority: 100,
tag: `li[data-type="${this.name}"]`
}
];
@@ -92,21 +94,37 @@ export const OutlineListItem = Node.create<ListItemOptions>({
return true;
});
},
Enter: () => {
// const subList = findSublist(editor, this.type);
// if (!subList) return this.editor.commands.splitListItem(this.name);
// const { isCollapsed, subListPos } = subList;
// if (isCollapsed) {
// return this.editor.commands.toggleOutlineCollapse(subListPos, false);
// }
Enter: ({ editor }) => {
const { $anchor } = editor.state.selection;
if (
$anchor.parent.type.name !== Paragraph.name ||
($anchor.parent.type.name === Paragraph.name &&
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
)
return false;
return this.editor.commands.splitListItem(this.name);
},
Tab: () => this.editor.commands.sinkListItem(this.name),
[keybindings.liftListItem.keys]: () =>
this.editor.commands.liftListItem(this.name)
Tab: ({ editor }) => {
const { $anchor } = editor.state.selection;
if (
$anchor.parent.type.name !== Paragraph.name ||
($anchor.parent.type.name === Paragraph.name &&
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
)
return false;
return this.editor.commands.sinkListItem(this.name);
},
[keybindings.liftListItem.keys]: ({ editor }) => {
const { $anchor } = editor.state.selection;
if (
$anchor.parent.type.name !== Paragraph.name ||
($anchor.parent.type.name === Paragraph.name &&
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
)
return false;
return this.editor.commands.liftListItem(this.name);
}
};
},
@@ -124,7 +142,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
function onClick(e: MouseEvent | TouchEvent) {
if (e instanceof MouseEvent && e.button !== 0) return;
if (!(e.target instanceof HTMLParagraphElement)) return;
if (!(e.target instanceof HTMLElement)) return;
if (!li.classList.contains("nested")) return;
const pos = typeof getPos === "function" ? getPos() : 0;

View File

@@ -536,11 +536,11 @@ p > *::selection {
.outline-list li.collapsed .outline-list {
display: none;
}
.outline-list li > :first-child {
.outline-list li {
position: relative;
}
.outline-list > li > :first-child::before {
.outline-list > li::before {
position: absolute;
top: 0px;
cursor: pointer;
@@ -560,13 +560,13 @@ p > *::selection {
left: -22px;
}
.outline-list li:not(.nested) > :first-child::before {
.outline-list li:not(.nested)::before {
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik0xMiAyQTEwIDEwIDAgMCAwIDIgMTJhMTAgMTAgMCAwIDAgMTAgMTBhMTAgMTAgMCAwIDAgMTAtMTBBMTAgMTAgMCAwIDAgMTIgMloiLz48L3N2Zz4=)
no-repeat 50% 50%;
scale: 0.4;
}
.outline-list li.collapsed > :first-child::before {
.outline-list li.collapsed::before {
transform: rotate(-90deg);
}

View File

@@ -92,3 +92,19 @@ export const p = elem("p");
export function text(text: string) {
return document.createTextNode(text);
}
export function outlineList(...children: HTMLLIElement[]) {
return ul(children, { "data-type": "outlineList" });
}
export function outlineListItem(
paragraphChildren: (string | HTMLElement)[],
subList?: HTMLUListElement
) {
const children: HTMLElement[] = [h("p", paragraphChildren)];
if (subList) children.push(subList);
return li(children, {
"data-type": "outlineListItem"
});
}