diff --git a/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html
new file mode 100644
index 0000000000..a261871741
--- /dev/null
+++ b/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html
@@ -0,0 +1 @@
+
Bullet List Item 1
Bullet List Item 2
Numbered List Item 1
Numbered List Item 2
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/__snapshots__/lists/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/lists/basic/internal.html
new file mode 100644
index 0000000000..bd48311268
--- /dev/null
+++ b/packages/core/src/api/exporters/html/__snapshots__/lists/basic/internal.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html
new file mode 100644
index 0000000000..9cfe048b34
--- /dev/null
+++ b/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html
@@ -0,0 +1 @@
+Bullet List Item 1
Bullet List Item 2
Numbered List Item 1
Numbered List Item 2
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/__snapshots__/lists/nested/internal.html b/packages/core/src/api/exporters/html/__snapshots__/lists/nested/internal.html
new file mode 100644
index 0000000000..c4026355d2
--- /dev/null
+++ b/packages/core/src/api/exporters/html/__snapshots__/lists/nested/internal.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
index 743d6ea391..d76b62340f 100644
--- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts
+++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
@@ -73,7 +73,10 @@ export const createExternalHTMLExporter = <
.use(rehypeParse, { fragment: true })
.use(simplifyBlocks, {
orderedListItemBlockTypes: new Set(["numberedListItem"]),
- unorderedListItemBlockTypes: new Set(["bulletListItem"]),
+ unorderedListItemBlockTypes: new Set([
+ "bulletListItem",
+ "checkListItem",
+ ]),
})
.use(rehypeStringify)
.processSync(serializeProseMirrorFragment(fragment, serializer));
diff --git a/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts b/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts
index ba025dc5b0..f6d59bb175 100644
--- a/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts
+++ b/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts
@@ -98,7 +98,7 @@ export function simplifyBlocks(options: SimplifyBlocksOptions) {
) as HASTElement;
// Adds only the content inside the block to the active list.
- listItemElement.children.push(blockContent.children[0]);
+ listItemElement.children.push(...blockContent.children);
// Nested blocks have already been processed in the recursive function call, so the resulting elements are
// also added to the active list.
if (blockGroup !== null) {
diff --git a/packages/core/src/api/exporters/markdown/__snapshots__/lists/basic/markdown.md b/packages/core/src/api/exporters/markdown/__snapshots__/lists/basic/markdown.md
new file mode 100644
index 0000000000..1af23eaaa9
--- /dev/null
+++ b/packages/core/src/api/exporters/markdown/__snapshots__/lists/basic/markdown.md
@@ -0,0 +1,8 @@
+* Bullet List Item 1
+* Bullet List Item 2
+
+1. Numbered List Item 1
+2. Numbered List Item 2
+
+* \[ ] Check List Item 1
+* \[x] Check List Item 2
diff --git a/packages/core/src/api/exporters/markdown/__snapshots__/lists/nested/markdown.md b/packages/core/src/api/exporters/markdown/__snapshots__/lists/nested/markdown.md
new file mode 100644
index 0000000000..f45a6354ea
--- /dev/null
+++ b/packages/core/src/api/exporters/markdown/__snapshots__/lists/nested/markdown.md
@@ -0,0 +1,10 @@
+* Bullet List Item 1
+
+* Bullet List Item 2
+
+ 1. Numbered List Item 1
+
+ 2. Numbered List Item 2
+
+ * \[ ] Check List Item 1
+ * \[x] Check List Item 2
diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts
index 24990edee3..ab802a88b2 100644
--- a/packages/core/src/api/exporters/markdown/markdownExporter.ts
+++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts
@@ -9,11 +9,13 @@ import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
import { createExternalHTMLExporter } from "../html/externalHTMLExporter";
import { removeUnderlines } from "./removeUnderlinesRehypePlugin";
+import { addSpacesToCheckboxes } from "./util/addSpacesToCheckboxesRehypePlugin";
export function cleanHTMLToMarkdown(cleanHTMLString: string) {
const markdownString = unified()
.use(rehypeParse, { fragment: true })
.use(removeUnderlines)
+ .use(addSpacesToCheckboxes)
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify)
diff --git a/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts
new file mode 100644
index 0000000000..bdfe2f6704
--- /dev/null
+++ b/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts
@@ -0,0 +1,42 @@
+import { Element as HASTElement, Parent as HASTParent } from "hast";
+import { fromDom } from "hast-util-from-dom";
+
+/**
+ * Rehype plugin which adds a space after each checkbox input element. This is
+ * because remark doesn't add any spaces between the checkbox input and the text
+ * itself, but these are needed for correct Markdown syntax.
+ */
+export function addSpacesToCheckboxes() {
+ const helper = (tree: HASTParent) => {
+ if (tree.children && "length" in tree.children && tree.children.length) {
+ for (let i = tree.children.length - 1; i >= 0; i--) {
+ const child = tree.children[i];
+ const nextChild =
+ i + 1 < tree.children.length ? tree.children[i + 1] : undefined;
+
+ // Checks for paragraph element after checkbox input element.
+ if (
+ child.type === "element" &&
+ child.tagName === "input" &&
+ child.properties?.type === "checkbox" &&
+ nextChild?.type === "element" &&
+ nextChild.tagName === "p"
+ ) {
+ // Converts paragraph to span, otherwise remark will think it needs to
+ // be on a new line.
+ nextChild.tagName = "span";
+ // Adds a space after the checkbox input element.
+ nextChild.children.splice(
+ 0,
+ 0,
+ fromDom(document.createTextNode(" ")) as HASTElement
+ );
+ } else {
+ helper(child as HASTParent);
+ }
+ }
+ }
+ };
+
+ return helper;
+}
diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
index d97a75e0db..3870c30a7a 100644
--- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
+++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
@@ -952,6 +952,56 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert
}
`;
+exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert lists/basic to/from prosemirror 1`] = `
+{
+ "attrs": {
+ "backgroundColor": "default",
+ "id": "1",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "attrs": {
+ "textAlignment": "left",
+ },
+ "content": [
+ {
+ "text": "Bullet List Item 1",
+ "type": "text",
+ },
+ ],
+ "type": "bulletListItem",
+ },
+ ],
+ "type": "blockContainer",
+}
+`;
+
+exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert lists/nested to/from prosemirror 1`] = `
+{
+ "attrs": {
+ "backgroundColor": "default",
+ "id": "1",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "attrs": {
+ "textAlignment": "left",
+ },
+ "content": [
+ {
+ "text": "Bullet List Item 1",
+ "type": "text",
+ },
+ ],
+ "type": "bulletListItem",
+ },
+ ],
+ "type": "blockContainer",
+}
+`;
+
exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/basic to/from prosemirror 1`] = `
{
"attrs": {
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json b/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json
index 7ef10bf491..67e64c4521 100644
--- a/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json
+++ b/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json
@@ -52,6 +52,42 @@
},
{
"id": "4",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Fourth",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "5",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Fifth",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "6",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -67,7 +103,7 @@
],
"children": [
{
- "id": "5",
+ "id": "7",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -84,7 +120,7 @@
"children": []
},
{
- "id": "6",
+ "id": "8",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -99,6 +135,42 @@
}
],
"children": []
+ },
+ {
+ "id": "9",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Child 3",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "10",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Child 4",
+ "styles": {}
+ }
+ ],
+ "children": []
}
]
}
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json
index 7bb12cd2cb..26371dc417 100644
--- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json
+++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json
@@ -14,9 +14,26 @@
"styles": {}
}
],
+ "children": []
+ },
+ {
+ "id": "2",
+ "type": "bulletListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Bullet List Item",
+ "styles": {}
+ }
+ ],
"children": [
{
- "id": "2",
+ "id": "3",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -33,7 +50,7 @@
"children": []
},
{
- "id": "3",
+ "id": "4",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -52,7 +69,7 @@
]
},
{
- "id": "4",
+ "id": "5",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -69,7 +86,78 @@
"children": []
},
{
- "id": "5",
+ "id": "6",
+ "type": "numberedListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "7",
+ "type": "numberedListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": [
+ {
+ "id": "8",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": true
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "9",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "10",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -83,9 +171,45 @@
"styles": {}
}
],
+ "children": []
+ },
+ {
+ "id": "11",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": true
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "12",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Check List Item",
+ "styles": {}
+ }
+ ],
"children": [
{
- "id": "6",
+ "id": "13",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -102,7 +226,7 @@
"children": []
},
{
- "id": "7",
+ "id": "14",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -121,17 +245,18 @@
]
},
{
- "id": "8",
- "type": "numberedListItem",
+ "id": "15",
+ "type": "checkListItem",
"props": {
"textColor": "default",
"backgroundColor": "default",
- "textAlignment": "left"
+ "textAlignment": "left",
+ "checked": true
},
"content": [
{
"type": "text",
- "text": "Numbered List Item",
+ "text": "Nested Check List Item",
"styles": {}
}
],
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json
index cc6065d2d4..0d3b65965d 100644
--- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json
+++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json
@@ -14,9 +14,26 @@
"styles": {}
}
],
+ "children": []
+ },
+ {
+ "id": "2",
+ "type": "bulletListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Bullet List Item",
+ "styles": {}
+ }
+ ],
"children": [
{
- "id": "2",
+ "id": "3",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -33,7 +50,7 @@
"children": []
},
{
- "id": "3",
+ "id": "4",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -52,7 +69,7 @@
]
},
{
- "id": "4",
+ "id": "5",
"type": "bulletListItem",
"props": {
"textColor": "default",
@@ -69,7 +86,24 @@
"children": []
},
{
- "id": "5",
+ "id": "6",
+ "type": "numberedListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "7",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -85,7 +119,7 @@
],
"children": [
{
- "id": "6",
+ "id": "8",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -102,7 +136,7 @@
"children": []
},
{
- "id": "7",
+ "id": "9",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -121,7 +155,7 @@
]
},
{
- "id": "8",
+ "id": "10",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -136,5 +170,96 @@
}
],
"children": []
+ },
+ {
+ "id": "11",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Checked List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "12",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Checked List Item",
+ "styles": {}
+ }
+ ],
+ "children": [
+ {
+ "id": "13",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Checked List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "14",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Checked List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "15",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Checked List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
}
]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json
index e20435c9c8..f617673f05 100644
--- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json
+++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json
@@ -100,9 +100,26 @@
"styles": {}
}
],
+ "children": []
+ },
+ {
+ "id": "7",
+ "type": "numberedListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Numbered List Item",
+ "styles": {}
+ }
+ ],
"children": [
{
- "id": "7",
+ "id": "8",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -119,7 +136,7 @@
"children": []
},
{
- "id": "8",
+ "id": "9",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -138,7 +155,7 @@
]
},
{
- "id": "9",
+ "id": "10",
"type": "numberedListItem",
"props": {
"textColor": "default",
@@ -153,5 +170,96 @@
}
],
"children": []
+ },
+ {
+ "id": "11",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "12",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": [
+ {
+ "id": "13",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "14",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "15",
+ "type": "checkListItem",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Nested Check List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
}
]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts
index 52951e2c26..36be3e30ea 100644
--- a/packages/core/src/api/parsers/html/parseHTML.test.ts
+++ b/packages/core/src/api/parsers/html/parseHTML.test.ts
@@ -76,88 +76,147 @@ describe("Parse HTML", () => {
it("list test", async () => {
const html = `
- First
- Second
- Third
- Five Parent
-
-
- `;
+ First
+ Second
+ Third
+
+
+ Fourth
+
+
+
+ Fifth
+
+ Five Parent
+
+
+ `;
await parseHTMLAndCompareSnapshots(html, "list-test");
});
it("Parse nested lists", async () => {
const html = `
Bullet List Item
- Bullet List Item
-
-
- Nested Bullet List Item
-
-
- Nested Bullet List Item
-
-
-
- Bullet List Item
-
+ Bullet List Item
+
+ Nested Bullet List Item
+ Nested Bullet List Item
+
+
+ Bullet List Item
-
- Numbered List Item
-
-
- Nested Numbered List Item
-
-
- Nested Numbered List Item
-
-
-
-
- Numbered List Item
-
- `;
+ Numbered List Item
+ Numbered List Item
+
+ Nested Numbered List Item
+ Nested Numbered List Item
+
+
+ Numbered List Item
+
+ `;
await parseHTMLAndCompareSnapshots(html, "parse-nested-lists");
});
it("Parse nested lists with paragraphs", async () => {
const html = `
-
- Bullet List Item
-
-
- Nested Bullet List Item
-
-
- Nested Bullet List Item
-
-
-
-
- Bullet List Item
-
+
+ Bullet List Item
+
+
+ Bullet List Item
+
+
+ Nested Bullet List Item
+
+
+ Nested Bullet List Item
+
+
+
+
+ Bullet List Item
+
-
- Numbered List Item
-
-
- Nested Numbered List Item
-
-
- Nested Numbered List Item
-
-
-
-
- Numbered List Item
-
- `;
+
+ Numbered List Item
+
+
+ Numbered List Item
+
+
+ Nested Numbered List Item
+
+
+ Nested Numbered List Item
+
+
+
+
+ Numbered List Item
+
+
+ `;
await parseHTMLAndCompareSnapshots(
html,
@@ -167,37 +226,49 @@ describe("Parse HTML", () => {
it("Parse mixed nested lists", async () => {
const html = `
-
- Bullet List Item
-
-
- Nested Numbered List Item
-
-
- Nested Numbered List Item
-
-
-
-
- Bullet List Item
-
+ Bullet List Item
+ Bullet List Item
+
+ Nested Numbered List Item
+ Nested Numbered List Item
+
+
+ Bullet List Item
-
- Numbered List Item
-
-
- Nested Bullet List Item
-
-
- Nested Bullet List Item
-
-
-
-
- Numbered List Item
-
- `;
+ Numbered List Item
+ Numbered List Item
+
+
+ Numbered List Item
+
+ `;
await parseHTMLAndCompareSnapshots(html, "parse-mixed-nested-lists");
});
diff --git a/packages/core/src/api/testUtil/cases/defaultSchema.ts b/packages/core/src/api/testUtil/cases/defaultSchema.ts
index 70564450a3..b69c9b1fa6 100644
--- a/packages/core/src/api/testUtil/cases/defaultSchema.ts
+++ b/packages/core/src/api/testUtil/cases/defaultSchema.ts
@@ -98,6 +98,74 @@ export const defaultSchemaTestCases: EditorTestCases<
},
],
},
+ {
+ name: "lists/basic",
+ blocks: [
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item 1",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item 2",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item 1",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item 2",
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item 1",
+ },
+ {
+ type: "checkListItem",
+ props: {
+ checked: true,
+ },
+ content: "Check List Item 2",
+ },
+ ],
+ },
+ {
+ name: "lists/nested",
+ blocks: [
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item 1",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item 2",
+ children: [
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item 1",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item 2",
+ children: [
+ {
+ type: "checkListItem",
+ content: "Check List Item 1",
+ },
+ {
+ type: "checkListItem",
+ props: {
+ checked: true,
+ },
+ content: "Check List Item 2",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
{
name: "file/button",
blocks: [
diff --git a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
index bdcbbb5eab..757dc50952 100644
--- a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
+++ b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
@@ -17,6 +17,9 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({
name: "bulletListItem",
content: "inline*",
group: "blockContent",
+ // This is to make sure that check list parse rules run before, since they
+ // both parse `li` elements but check lists are more specific.
+ priority: 90,
addInputRules() {
return [
// Creates an unordered list when starting with "-", "+", or "*".
diff --git a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts
new file mode 100644
index 0000000000..1e7648fecf
--- /dev/null
+++ b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts
@@ -0,0 +1,266 @@
+import { InputRule } from "@tiptap/core";
+import {
+ PropSchema,
+ createBlockSpecFromStronglyTypedTiptapNode,
+ createStronglyTypedTiptapNode,
+} from "../../../schema";
+import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers";
+import { defaultProps } from "../../defaultProps";
+import { handleEnter } from "../ListItemKeyboardShortcuts";
+import { getCurrentBlockContentType } from "../../../api/getCurrentBlockContentType";
+
+export const checkListItemPropSchema = {
+ ...defaultProps,
+ checked: {
+ default: false,
+ },
+} satisfies PropSchema;
+
+const checkListItemBlockContent = createStronglyTypedTiptapNode({
+ name: "checkListItem",
+ content: "inline*",
+ group: "blockContent",
+ addAttributes() {
+ return {
+ checked: {
+ default: false,
+ // instead of "checked" attributes, use "data-checked"
+ parseHTML: (element) =>
+ element.getAttribute("data-checked") === "true" || undefined,
+ renderHTML: (attributes) => {
+ return attributes.checked
+ ? {
+ "data-checked": (attributes.checked as boolean).toString(),
+ }
+ : {};
+ },
+ },
+ };
+ },
+
+ addInputRules() {
+ return [
+ // Creates a checklist when starting with "[]" or "[X]".
+ new InputRule({
+ find: new RegExp(`\\[\\s*\\]\\s$`),
+ handler: ({ state, chain, range }) => {
+ if (getCurrentBlockContentType(this.editor) !== "inline*") {
+ return;
+ }
+
+ chain()
+ .BNUpdateBlock(state.selection.from, {
+ type: "checkListItem",
+ props: {
+ checked: false as any,
+ },
+ })
+ // Removes the characters used to set the list.
+ .deleteRange({ from: range.from, to: range.to });
+ },
+ }),
+ new InputRule({
+ find: new RegExp(`\\[[Xx]\\]\\s$`),
+ handler: ({ state, chain, range }) => {
+ if (getCurrentBlockContentType(this.editor) !== "inline*") {
+ return;
+ }
+
+ chain()
+ .BNUpdateBlock(state.selection.from, {
+ type: "checkListItem",
+ props: {
+ checked: true as any,
+ },
+ })
+ // Removes the characters used to set the list.
+ .deleteRange({ from: range.from, to: range.to });
+ },
+ }),
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Enter: () => handleEnter(this.editor),
+ "Mod-Shift-9": () => {
+ if (getCurrentBlockContentType(this.editor) !== "inline*") {
+ return true;
+ }
+
+ return this.editor.commands.BNUpdateBlock(
+ this.editor.state.selection.anchor,
+ {
+ type: "checkListItem",
+ props: {},
+ }
+ );
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this
+ },
+ // Checkbox only.
+ {
+ tag: "input",
+ getAttrs: (element) => {
+ if (typeof element === "string") {
+ return false;
+ }
+
+ if ((element as HTMLInputElement).type === "checkbox") {
+ return { checked: (element as HTMLInputElement).checked };
+ }
+
+ return false;
+ },
+ node: "checkListItem",
+ },
+ // Container element for checkbox + label.
+ {
+ tag: "li",
+ getAttrs: (element) => {
+ if (typeof element === "string") {
+ return false;
+ }
+
+ const parent = element.parentElement;
+
+ if (parent === null) {
+ return false;
+ }
+
+ if (
+ parent.tagName === "UL" ||
+ (parent.tagName === "DIV" && parent.parentElement!.tagName === "UL")
+ ) {
+ const checkbox =
+ (element.querySelector(
+ "input[type=checkbox]"
+ ) as HTMLInputElement) || null;
+
+ if (checkbox === null) {
+ return false;
+ }
+
+ return { checked: checkbox.checked };
+ }
+
+ return false;
+ },
+ node: "checkListItem",
+ },
+ ];
+ },
+
+ // Since there is no HTML checklist element, there isn't really any
+ // standardization for what checklists should look like in the DOM. GDocs'
+ // and Notion's aren't cross compatible, for example. This implementation
+ // has a semantically correct DOM structure (though missing a label for the
+ // checkbox) which is also converted correctly to Markdown by remark.
+ renderHTML({ node, HTMLAttributes }) {
+ const checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.checked = node.attrs.checked;
+ if (node.attrs.checked) {
+ checkbox.setAttribute("checked", "");
+ }
+
+ const { dom, contentDOM } = createDefaultBlockDOMOutputSpec(
+ this.name,
+ "p",
+ {
+ ...(this.options.domAttributes?.blockContent || {}),
+ ...HTMLAttributes,
+ },
+ this.options.domAttributes?.inlineContent || {}
+ );
+
+ dom.insertBefore(checkbox, contentDOM);
+
+ return { dom, contentDOM };
+ },
+
+ // Need to render node view since the checkbox needs to be able to update the
+ // node. This is only possible with a node view as it exposes `getPos`.
+ addNodeView() {
+ return ({ node, getPos, editor, HTMLAttributes }) => {
+ // Need to wrap certain elements in a div or keyboard navigation gets
+ // confused.
+ const wrapper = document.createElement("div");
+ const checkboxWrapper = document.createElement("div");
+ checkboxWrapper.contentEditable = "false";
+
+ const checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.checked = node.attrs.checked;
+ if (node.attrs.checked) {
+ checkbox.setAttribute("checked", "");
+ }
+
+ const changeHandler = () => {
+ if (!editor.isEditable) {
+ // This seems like the most effective way of blocking the checkbox
+ // from being toggled, as event.preventDefault() does not stop it for
+ // "click" or "change" events.
+ checkbox.checked = !checkbox.checked;
+ return;
+ }
+
+ if (typeof getPos !== "boolean") {
+ this.editor.commands.BNUpdateBlock(getPos(), {
+ type: "checkListItem",
+ props: {
+ checked: checkbox.checked as any,
+ },
+ });
+ }
+ };
+ checkbox.addEventListener("change", changeHandler);
+
+ const { dom, contentDOM } = createDefaultBlockDOMOutputSpec(
+ this.name,
+ "p",
+ {
+ ...(this.options.domAttributes?.blockContent || {}),
+ ...HTMLAttributes,
+ },
+ this.options.domAttributes?.inlineContent || {}
+ );
+
+ if (typeof getPos !== "boolean") {
+ // Since `node` is a blockContent node, we have to get the block ID from
+ // the parent blockContainer node. This means we can't add the label in
+ // `renderHTML` as we can't use `getPos` and therefore can't get the
+ // parent blockContainer node.
+ const blockID = this.editor.state.doc.resolve(getPos()).node().attrs.id;
+ const label = "label-" + blockID;
+ checkbox.setAttribute("aria-labelledby", label);
+ contentDOM.id = label;
+ }
+
+ dom.removeChild(contentDOM);
+ dom.appendChild(wrapper);
+ wrapper.appendChild(checkboxWrapper);
+ wrapper.appendChild(contentDOM);
+ checkboxWrapper.appendChild(checkbox);
+
+ return {
+ dom,
+ contentDOM,
+ destroy: () => {
+ checkbox.removeEventListener("change", changeHandler);
+ },
+ };
+ };
+ },
+});
+
+export const CheckListItem = createBlockSpecFromStronglyTypedTiptapNode(
+ checkListItemBlockContent,
+ checkListItemPropSchema
+);
diff --git a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts
index bb7eb27525..118f32f37f 100644
--- a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts
+++ b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts
@@ -13,7 +13,8 @@ export const handleEnter = (editor: Editor) => {
if (
!(
contentType.name === "bulletListItem" ||
- contentType.name === "numberedListItem"
+ contentType.name === "numberedListItem" ||
+ contentType.name === "checkListItem"
) ||
!selectionEmpty
) {
diff --git a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
index a2fd3b4142..0ea2481c70 100644
--- a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
+++ b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
@@ -18,6 +18,7 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
name: "numberedListItem",
content: "inline*",
group: "blockContent",
+ priority: 90,
addAttributes() {
return {
index: {
diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts
index 8bde3a6077..e2b9b9e8dc 100644
--- a/packages/core/src/blocks/defaultBlocks.ts
+++ b/packages/core/src/blocks/defaultBlocks.ts
@@ -19,13 +19,15 @@ import {
getInlineContentSchemaFromSpecs,
getStyleSchemaFromSpecs,
} from "../schema";
-import { FileBlock } from "./FileBlockContent/FileBlockContent";
-import { ImageBlock } from "./ImageBlockContent/ImageBlockContent";
+
import { Heading } from "./HeadingBlockContent/HeadingBlockContent";
import { BulletListItem } from "./ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent";
import { NumberedListItem } from "./ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent";
+import { CheckListItem } from "./ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent";
import { Paragraph } from "./ParagraphBlockContent/ParagraphBlockContent";
import { Table } from "./TableBlockContent/TableBlockContent";
+import { FileBlock } from "./FileBlockContent/FileBlockContent";
+import { ImageBlock } from "./ImageBlockContent/ImageBlockContent";
import { VideoBlock } from "./VideoBlockContent/VideoBlockContent";
import { AudioBlock } from "./AudioBlockContent/AudioBlockContent";
@@ -34,11 +36,12 @@ export const defaultBlockSpecs = {
heading: Heading,
bulletListItem: BulletListItem,
numberedListItem: NumberedListItem,
+ checkListItem: CheckListItem,
+ table: Table,
file: FileBlock,
image: ImageBlock,
video: VideoBlock,
audio: AudioBlock,
- table: Table,
} satisfies BlockSpecs;
export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs);
diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css
index b8ee3d578d..7c2d968894 100644
--- a/packages/core/src/editor/Block.css
+++ b/packages/core/src/editor/Block.css
@@ -185,6 +185,28 @@ NESTED BLOCKS
gap: 1.2em;
}
+/* Checked */
+.bn-block-content[data-content-type="checkListItem"] > div {
+ display: flex;
+}
+
+.bn-block-content[data-content-type="checkListItem"] > div > div > input {
+ margin: 0 1.2em 0 0;
+ cursor: pointer;
+}
+
+.bn-block-content[data-content-type="checkListItem"][data-checked="true"] .bn-inline-content {
+ text-decoration: line-through;
+}
+
+.bn-block-content[data-text-alignment="center"] {
+ justify-content: center;
+}
+
+.bn-block-content[data-text-alignment="right"] {
+ justify-content: flex-end;
+}
+
/* No list nesting */
.bn-block-outer[data-prev-type="bulletListItem"]
> .bn-block
diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts
index ac3d2eb578..cea35c8e04 100644
--- a/packages/core/src/editor/transformPasted.ts
+++ b/packages/core/src/editor/transformPasted.ts
@@ -40,7 +40,8 @@ export function transformPasted(slice: Slice, view: EditorView) {
if (
nestedChild.type.name === "bulletListItem" ||
- nestedChild.type.name === "numberedListItem"
+ nestedChild.type.name === "numberedListItem" ||
+ nestedChild.type.name === "checkListItem"
) {
content.push(f.child(i + 1));
f = removeChild(f, i + 1);
diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
index eb41dddcdd..36fc89ffbf 100644
--- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
+++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
@@ -144,6 +144,19 @@ export function getDefaultSlashMenuItems<
});
}
+ if (checkDefaultBlockTypeInSchema("checkListItem", editor)) {
+ items.push({
+ onItemClick: () => {
+ insertOrUpdateBlock(editor, {
+ type: "checkListItem",
+ });
+ },
+ badge: formatKeyboardShortcut("Mod-Shift-9"),
+ key: "check_list",
+ ...editor.dictionary.slash_menu.check_list,
+ });
+ }
+
if (checkDefaultBlockTypeInSchema("paragraph", editor)) {
items.push({
onItemClick: () => {
diff --git a/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts b/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts
index 7f9fb505ea..770fbb35ca 100644
--- a/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts
+++ b/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts
@@ -8,7 +8,13 @@ export const TextAlignmentExtension = Extension.create({
{
// Attribute is applied to block content instead of container so that child blocks don't inherit the text
// alignment styling.
- types: ["paragraph", "heading", "bulletListItem", "numberedListItem"],
+ types: [
+ "paragraph",
+ "heading",
+ "bulletListItem",
+ "numberedListItem",
+ "checkListItem",
+ ],
attributes: {
textAlignment: {
default: "left",
diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts
index 4613fb7081..9edbb6df20 100644
--- a/packages/core/src/i18n/locales/en.ts
+++ b/packages/core/src/i18n/locales/en.ts
@@ -30,6 +30,20 @@ export const en = {
aliases: ["ul", "li", "list", "bulletlist", "bullet list"],
group: "Basic blocks",
},
+ check_list: {
+ title: "Check List",
+ subtext: "Used to display a list with checkboxes",
+ aliases: [
+ "ul",
+ "li",
+ "list",
+ "checklist",
+ "check list",
+ "checked list",
+ "checkbox",
+ ],
+ group: "Basic blocks",
+ },
paragraph: {
title: "Paragraph",
subtext: "Used for the body of your document",
@@ -96,6 +110,7 @@ export const en = {
heading: "Heading",
bulletListItem: "List",
numberedListItem: "List",
+ checkListItem: "List",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts
index 57a31e95ed..c4345c6631 100644
--- a/packages/core/src/i18n/locales/fr.ts
+++ b/packages/core/src/i18n/locales/fr.ts
@@ -32,6 +32,19 @@ export const fr: Dictionary = {
aliases: ["ul", "li", "liste", "listeàpuces", "liste à puces"],
group: "Blocs de base",
},
+ check_list: {
+ title: "Liste de vérification",
+ subtext: "Utilisé pour afficher une liste avec des cases à cocher",
+ aliases: [
+ "ul",
+ "li",
+ "liste",
+ "liste de vérification",
+ "liste cochée",
+ "case à cocher",
+ ],
+ group: "Blocs de base",
+ },
paragraph: {
title: "Paragraphe",
subtext: "Utilisé pour le corps de votre document",
@@ -98,6 +111,7 @@ export const fr: Dictionary = {
heading: "Titre",
bulletListItem: "Liste",
numberedListItem: "Liste",
+ checkListItem: "Liste",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts
index f61198b6f1..729dc91ddc 100644
--- a/packages/core/src/i18n/locales/is.ts
+++ b/packages/core/src/i18n/locales/is.ts
@@ -32,6 +32,12 @@ export const is: Dictionary = {
aliases: ["ul", "li", "listi", "punktalisti"],
group: "Grunnblokkar",
},
+ check_list: {
+ title: "Athugunarlisti",
+ subtext: "Notað til að sýna lista með gátreitum",
+ aliases: ["ul", "li", "listi", "athugunarlisti", "merktur listi"],
+ group: "Grunnblokkar",
+ },
paragraph: {
title: "Málsgrein",
subtext: "Notað fyrir meginmál skjalsins",
@@ -98,6 +104,7 @@ export const is: Dictionary = {
heading: "Fyrirsögn",
bulletListItem: "Listi",
numberedListItem: "Listi",
+ checkListItem: "Listi",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts
index e77fe831fd..59dbc267ad 100644
--- a/packages/core/src/i18n/locales/ja.ts
+++ b/packages/core/src/i18n/locales/ja.ts
@@ -37,7 +37,29 @@ export const ja: Dictionary = {
bullet_list: {
title: "箇条書き",
subtext: "箇条書きを表示するために使用",
- aliases: ["ul", "li", "リスト", "箇条書きリスト"],
+ aliases: [
+ "ul",
+ "li",
+ "bulletlist",
+ "bullet list",
+ "リスト",
+ "箇条書きリスト",
+ ],
+ group: "基本ブロック",
+ },
+ check_list: {
+ title: "チェックリスト",
+ subtext: "チェックボックス付きリストを表示するために使用されます",
+ aliases: [
+ "ul",
+ "li",
+ "list",
+ "checklist",
+ "checked list",
+ "リスト",
+ "チェックリスト",
+ "チェックされたリスト",
+ ],
group: "基本ブロック",
},
paragraph: {
@@ -109,6 +131,7 @@ export const ja: Dictionary = {
heading: "見出し",
bulletListItem: "リストを追加",
numberedListItem: "リストを追加",
+ checkListItem: "リストを追加",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts
index 888a062f33..20127fc976 100644
--- a/packages/core/src/i18n/locales/ko.ts
+++ b/packages/core/src/i18n/locales/ko.ts
@@ -32,6 +32,20 @@ export const ko: Dictionary = {
aliases: ["ul", "li", "목록", "글머리 기호 목록", "글머리 목록"],
group: "기본 블록",
},
+ check_list: {
+ title: "체크리스트",
+ subtext: "체크박스가 있는 목록을 표시하는 데 사용",
+ aliases: [
+ "ul",
+ "li",
+ "목록",
+ "체크리스트",
+ "체크 리스트",
+ "체크된 목록",
+ "체크박스",
+ ],
+ group: "기본 블록",
+ },
paragraph: {
title: "본문",
subtext: "일반 텍스트",
@@ -101,6 +115,7 @@ export const ko: Dictionary = {
heading: "제목",
bulletListItem: "목록",
numberedListItem: "목록",
+ checkListItem: "목록",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts
index 0d9e5f3628..1cd8cf747a 100644
--- a/packages/core/src/i18n/locales/nl.ts
+++ b/packages/core/src/i18n/locales/nl.ts
@@ -32,6 +32,12 @@ export const nl: Dictionary = {
aliases: ["ul", "li", "lijst", "puntenlijst", "punten lijst"],
group: "Basisblokken",
},
+ check_list: {
+ title: "Controlelijst",
+ subtext: "Gebruikt om een lijst met selectievakjes weer te geven",
+ aliases: ["ul", "li", "lijst", "aangevinkte lijst", "selectievakje"],
+ group: "Basisblokken",
+ },
paragraph: {
title: "Paragraaf",
subtext: "Gebruikt voor de hoofdtekst van uw document",
@@ -100,6 +106,7 @@ export const nl: Dictionary = {
heading: "Kop",
bulletListItem: "Lijst",
numberedListItem: "Lijst",
+ checkListItem: "Lijst",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts
index d5d6cba04b..e5647c1b31 100644
--- a/packages/core/src/i18n/locales/pl.ts
+++ b/packages/core/src/i18n/locales/pl.ts
@@ -32,6 +32,12 @@ export const pl: Dictionary = {
aliases: ["ul", "li", "lista", "punktowana lista"],
group: "Podstawowe bloki",
},
+ check_list: {
+ title: "Lista z polami wyboru",
+ subtext: "Używana do wyświetlania listy z polami wyboru",
+ aliases: ["ul", "li", "lista", "lista z polami wyboru", "pole wyboru"],
+ group: "Podstawowe bloki",
+ },
paragraph: {
title: "Akapit",
subtext: "Używany dla treści dokumentu",
@@ -90,6 +96,7 @@ export const pl: Dictionary = {
heading: "Nagłówek",
bulletListItem: "Lista",
numberedListItem: "Lista",
+ checkListItem: "Lista",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts
index bfd374231b..e03bc1d446 100644
--- a/packages/core/src/i18n/locales/pt.ts
+++ b/packages/core/src/i18n/locales/pt.ts
@@ -32,6 +32,19 @@ export const pt: Dictionary = {
aliases: ["ul", "li", "lista", "listamarcadores", "lista com marcadores"],
group: "Blocos Básicos",
},
+ check_list: {
+ title: "Lista de verificação",
+ subtext: "Usado para exibir uma lista com caixas de seleção",
+ aliases: [
+ "ul",
+ "li",
+ "lista",
+ "lista de verificação",
+ "lista marcada",
+ "caixa de seleção",
+ ],
+ group: "Blocos básicos",
+ },
paragraph: {
title: "Parágrafo",
subtext: "Usado para o corpo do seu documento",
@@ -90,6 +103,7 @@ export const pt: Dictionary = {
heading: "Título",
bulletListItem: "Lista",
numberedListItem: "Lista",
+ checkListItem: "Lista",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts
index 6e58aa206b..054076f321 100644
--- a/packages/core/src/i18n/locales/vi.ts
+++ b/packages/core/src/i18n/locales/vi.ts
@@ -23,13 +23,26 @@ export const vi: Dictionary = {
numbered_list: {
title: "Danh sách đánh số",
subtext: "Sử dụng để hiển thị danh sách có đánh số",
- aliases: ["ol", "li", "ds", "danhsachdso", "danh sách đánh số"],
+ aliases: ["ol", "li", "ds", "danhsachdso", "danh sach danh so"],
group: "Khối cơ bản",
},
bullet_list: {
title: "Danh sách",
subtext: "Sử dụng để hiển thị danh sách không đánh số",
- aliases: ["ul", "li", "ds", "danhsach", "danh sách"],
+ aliases: ["ul", "li", "ds", "danhsach", "danh sach"],
+ group: "Khối cơ bản",
+ },
+ check_list: {
+ title: "Danh sách kiểm tra",
+ subtext: "Dùng để hiển thị danh sách có hộp kiểm",
+ aliases: [
+ "ul",
+ "li",
+ "danh sach",
+ "danh sach kiem tra",
+ "danh sach da kiem tra",
+ "hop kiem",
+ ],
group: "Khối cơ bản",
},
paragraph: {
@@ -90,6 +103,7 @@ export const vi: Dictionary = {
heading: "Tiêu đề",
bulletListItem: "Danh sách",
numberedListItem: "Danh sách",
+ checkListItem: "Danh sách",
},
file_blocks: {
image: {
diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts
index 7e173a3828..324fa54e19 100644
--- a/packages/core/src/i18n/locales/zh.ts
+++ b/packages/core/src/i18n/locales/zh.ts
@@ -48,6 +48,21 @@ export const zh: Dictionary = {
],
group: "基础",
},
+ check_list: {
+ title: "检查清单",
+ subtext: "用于显示带有复选框的列表",
+ aliases: [
+ "ul",
+ "li",
+ "checklist",
+ "checked list",
+ "列表",
+ "检查清单",
+ "勾选列表",
+ "复选框",
+ ],
+ group: "基本块",
+ },
paragraph: {
title: "段落",
subtext: "用于文档正文",
@@ -121,6 +136,7 @@ export const zh: Dictionary = {
heading: "标题",
bulletListItem: "列表",
numberedListItem: "列表",
+ checkListItem: "列表",
},
file_blocks: {
image: {
diff --git a/packages/core/src/pm-nodes/BlockContainer.ts b/packages/core/src/pm-nodes/BlockContainer.ts
index fa1e7ee66b..9050119f80 100644
--- a/packages/core/src/pm-nodes/BlockContainer.ts
+++ b/packages/core/src/pm-nodes/BlockContainer.ts
@@ -36,7 +36,11 @@ declare module "@tiptap/core" {
BNCreateBlock: (pos: number) => ReturnType;
BNDeleteBlock: (posInBlock: number) => ReturnType;
BNMergeBlocks: (posBetweenBlocks: number) => ReturnType;
- BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType;
+ BNSplitBlock: (
+ posInBlock: number,
+ keepType?: boolean,
+ keepProps?: boolean
+ ) => ReturnType;
BNUpdateBlock: <
BSchema extends BlockSchema,
I extends InlineContentSchema,
@@ -402,8 +406,12 @@ export const BlockContainer = Node.create<{
},
// Splits a block at a given position. Content after the position is moved to a new block below, at the same
// nesting level.
+ // - `keepType` is usually false, unless the selection is at the start of
+ // a block.
+ // - `keepProps` is usually true when `keepType` is true, except for when
+ // creating new list item blocks with Enter.
BNSplitBlock:
- (posInBlock, keepType) =>
+ (posInBlock, keepType, keepProps) =>
({ state, dispatch }) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) {
@@ -448,7 +456,7 @@ export const BlockContainer = Node.create<{
newBlockContentPos,
newBlockContentPos,
state.schema.node(contentType).type,
- contentNode.attrs
+ keepProps ? contentNode.attrs : undefined
);
}
@@ -671,7 +679,11 @@ export const BlockContainer = Node.create<{
if (!blockEmpty) {
chain()
.deleteSelection()
- .BNSplitBlock(state.selection.from, selectionAtBlockStart)
+ .BNSplitBlock(
+ state.selection.from,
+ selectionAtBlockStart,
+ selectionAtBlockStart
+ )
.run();
return true;
diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
index 69baeb1a13..ac33192b73 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
@@ -11,6 +11,7 @@ import {
RiH1,
RiH2,
RiH3,
+ RiListCheck3,
RiListOrdered,
RiListUnordered,
RiText,
@@ -86,6 +87,12 @@ export const blockTypeSelectItems = (
icon: RiListOrdered,
isSelected: (block) => block.type === "numberedListItem",
},
+ {
+ name: dict.slash_menu.check_list.title,
+ type: "checkListItem",
+ icon: RiListCheck3,
+ isSelected: (block) => block.type === "checkListItem",
+ },
];
export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx
index 6219177e4d..88fbe29bab 100644
--- a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx
+++ b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx
@@ -11,6 +11,7 @@ import {
RiH3,
RiFile2Line,
RiImage2Fill,
+ RiListCheck3,
RiListOrdered,
RiListUnordered,
RiTable2,
@@ -26,6 +27,7 @@ const icons = {
heading_3: RiH3,
numbered_list: RiListOrdered,
bullet_list: RiListUnordered,
+ check_list: RiListCheck3,
paragraph: RiText,
table: RiTable2,
image: RiImage2Fill,
diff --git a/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-chromium-linux.png b/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-chromium-linux.png
index 02b42f3a91..e0d5d27b4f 100644
Binary files a/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-chromium-linux.png and b/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-chromium-linux.png differ
diff --git a/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-firefox-linux.png b/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-firefox-linux.png
index 1dfd5a694d..ba8673918e 100644
Binary files a/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-firefox-linux.png and b/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-firefox-linux.png differ
diff --git a/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-webkit-linux.png b/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-webkit-linux.png
index eb10921a40..ef24c8fee2 100644
Binary files a/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-webkit-linux.png and b/tests/src/end-to-end/ariakit/ariakit.test.ts-snapshots/ariakit-slash-menu-webkit-linux.png differ
diff --git a/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-chromium-linux.png b/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-chromium-linux.png
index aea6c5abe2..cb008535d1 100644
Binary files a/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-chromium-linux.png and b/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-chromium-linux.png differ
diff --git a/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-firefox-linux.png b/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-firefox-linux.png
index fa441631ba..e380edf1d8 100644
Binary files a/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-firefox-linux.png and b/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-firefox-linux.png differ
diff --git a/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-webkit-linux.png b/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-webkit-linux.png
index 4e4a02dbd3..c7b9713555 100644
Binary files a/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-webkit-linux.png and b/tests/src/end-to-end/shadcn/shadcn.test.ts-snapshots/shadcn-slash-menu-webkit-linux.png differ
diff --git a/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-chromium-linux.png b/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-chromium-linux.png
index 074bb98ea0..68927f404e 100644
Binary files a/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-chromium-linux.png and b/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-chromium-linux.png differ
diff --git a/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-firefox-linux.png b/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-firefox-linux.png
index a45d4efeaa..78837a6c7d 100644
Binary files a/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-firefox-linux.png and b/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-firefox-linux.png differ
diff --git a/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-webkit-linux.png b/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-webkit-linux.png
index 21c31e557b..66f5a2b403 100644
Binary files a/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-webkit-linux.png and b/tests/src/end-to-end/theming/theming.test.ts-snapshots/dark-slash-menu-webkit-linux.png differ