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 @@ +
  1. Numbered List Item 1

  2. 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 @@ +

Bullet List Item 1

Bullet List Item 2

Numbered List Item 1

Numbered List Item 2

Check List Item 1

Check List Item 2

\ 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 @@ + \ 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 @@ +

Bullet List Item 1

Bullet List Item 2

Numbered List Item 1

Numbered List Item 2

Check List Item 1

Check List Item 2

\ 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 -
      -
    • Child 1
    • -
    • Child 2
    • -
    -
  • -
`; +
  • First
  • +
  • Second
  • +
  • Third
  • +
  • + + Fourth +
  • +
  • + + Fifth +
  • +
  • Five Parent +
      +
    • Child 1
    • +
    • Child 2
    • +
    • + + Child 3 +
    • +
    • + + Child 4 +
    • +
    +
  • + `; 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
      -
    1. - Numbered List Item -
        -
      1. - Nested Numbered List Item -
      2. -
      3. - Nested Numbered List Item -
      4. -
      -
    2. -
    3. - Numbered List Item -
    4. -
    `; +
  • Numbered List Item
  • +
  • Numbered List Item +
      +
    1. Nested Numbered List Item
    2. +
    3. Nested Numbered List Item
    4. +
    +
  • +
  • Numbered List Item
  • + +
      +
    • + + Check List Item +
    • +
    • + + Check List Item +
        +
      • + + Nested Check List Item +
      • +
      • + + Nested Check List Item +
      • +
      +
    • +
    • + + Nested Check 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

      +
      -
    1. -

      Numbered List Item

      -
        -
      1. -

        Nested Numbered List Item

        -
      2. -
      3. -

        Nested Numbered List Item

        -
      4. -
      -
    2. -
    3. -

      Numbered List Item

      -
    4. -
    `; +
  • +

    Numbered List Item

    +
  • +
  • +

    Numbered List Item

    +
      +
    1. +

      Nested Numbered List Item

      +
    2. +
    3. +

      Nested Numbered List Item

      +
    4. +
    +
  • +
  • +

    Numbered List Item

    +
  • + +
      +
    • + +

      Checked List Item

      +
    • +
    • + +

      Checked List Item

      +
        +
      • + +

        Nested Checked List Item

        +
      • +
      • + +

        Nested Checked List Item

        +
      • +
      +
    • +
    • + +

      Checked List Item

      +
    • +
    `; await parseHTMLAndCompareSnapshots( html, @@ -167,37 +226,49 @@ describe("Parse HTML", () => { it("Parse mixed nested lists", async () => { const html = `
      -
    • - Bullet List Item -
        -
      1. - Nested Numbered List Item -
      2. -
      3. - Nested Numbered List Item -
      4. -
      -
    • -
    • - Bullet List Item -
    • +
    • Bullet List Item
    • +
    • Bullet List Item +
        +
      1. Nested Numbered List Item
      2. +
      3. Nested Numbered List Item
      4. +
      +
    • +
    • Bullet List Item
      -
    1. - Numbered List Item -
        -
      • -

        Nested Bullet List Item

        -
      • -
      • -

        Nested Bullet List Item

        -
      • -
      -
    2. -
    3. - Numbered List Item -
    4. -
    `; +
  • Numbered List Item
  • +
  • Numbered List Item +
      +
    • + + Nested Check List Item +
    • +
    • + + Nested Check List Item +
    • +
    +
  • +
  • Numbered List Item
  • + +
      +
    • + + Check List Item +
    • +
    • + + Check List Item +
        +
      • Nested Bullet List Item
      • +
      • Nested Bullet List Item
      • +
      +
    • +
    • + + Nested Check 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