Skip to content

Commit 196d4aa

Browse files
YousefEDareknawomatthewlipski
authored
feat: list "start" index and add optional props (#1326)
* fix: Add <table> wrapping element when copying * fix: Allow different start index for ordered lists * fix: Parsing ordered list start index from HTML * feat: Start attribute for numbered list item * fix: Use start attribute for HTML serialization * fix: Numbered lists start index handling * test: Update snapshots * add optional props * small fixes * add docs * fix build * fix * fix: Remove start attrib when duplicated in consecutive list * fix: Setting index on list items when starting at 1 * fix: Remove start attrib when lists are merged * Updated e2e tests * Updated e2e tests * Updated typing --------- Co-authored-by: Arek Nawo <[email protected]> Co-authored-by: matthewlipski <[email protected]>
1 parent 7ff9876 commit 196d4aa

File tree

19 files changed

+157
-84
lines changed

19 files changed

+157
-84
lines changed

packages/core/src/api/clipboard/toClipboard/copyExtension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ function fragmentToExternalHTML<
8080
editor.schema.styleSchema
8181
);
8282

83+
// Wrap in table to ensure correct parsing by spreadsheet applications
8384
externalHTML = `<table>${externalHTMLExporter.exportInlineContent(
8485
ic as any,
8586
{}

packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ function serializeBlock<
104104
for (const [name, spec] of Object.entries(
105105
editor.schema.blockSchema[block.type as any].propSchema
106106
)) {
107-
(props as any)[name] = spec.default;
107+
if (spec.default !== undefined) {
108+
(props as any)[name] = spec.default;
109+
}
108110
}
109111
}
110112

@@ -172,6 +174,10 @@ function serializeBlock<
172174
if (listType) {
173175
if (fragment.lastChild?.nodeName !== listType) {
174176
const list = doc.createElement(listType);
177+
178+
if (listType === "OL" && props?.start && props?.start !== 1) {
179+
list.setAttribute("start", props.start + "");
180+
}
175181
fragment.append(list);
176182
}
177183
const li = doc.createElement("li");

packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ function serializeBlock<
7878
for (const [name, spec] of Object.entries(
7979
editor.schema.blockSchema[block.type as any].propSchema
8080
)) {
81-
(props as any)[name] = spec.default;
81+
if (spec.default !== undefined) {
82+
(props as any)[name] = spec.default;
83+
}
8284
}
8385
}
8486

packages/core/src/api/nodeConversions/nodeToBlock.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,10 @@ export function nodeToBlock<
355355
})) {
356356
const propSchema = blockSpec.propSchema;
357357

358-
if (attr in propSchema) {
358+
if (
359+
attr in propSchema &&
360+
!(propSchema[attr].default === undefined && value === undefined)
361+
) {
359362
props[attr] = value;
360363
}
361364
}

packages/core/src/api/testUtil/partialBlockTestUtil.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ export function partialBlockToBlockForTesting<
111111

112112
Object.entries(schema[partialBlock.type!].propSchema).forEach(
113113
([propKey, propValue]) => {
114-
if (withDefaults.props[propKey] === undefined) {
114+
if (
115+
withDefaults.props[propKey] === undefined &&
116+
propValue.default !== undefined
117+
) {
115118
(withDefaults.props as any)[propKey] = propValue.default;
116119
}
117120
}

packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
PropSchema,
66
createBlockSpecFromStronglyTypedTiptapNode,
77
createStronglyTypedTiptapNode,
8+
propsToAttributes,
89
} from "../../schema/index.js";
910
import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js";
1011
import { defaultProps } from "../defaultProps.js";
@@ -18,26 +19,9 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({
1819
name: "heading",
1920
content: "inline*",
2021
group: "blockContent",
22+
2123
addAttributes() {
22-
return {
23-
level: {
24-
default: 1,
25-
// instead of "level" attributes, use "data-level"
26-
parseHTML: (element) => {
27-
const attr = element.getAttribute("data-level")!;
28-
const parsed = parseInt(attr);
29-
if (isFinite(parsed)) {
30-
return parsed;
31-
}
32-
return undefined;
33-
},
34-
renderHTML: (attributes) => {
35-
return {
36-
"data-level": (attributes.level as number).toString(),
37-
};
38-
},
39-
},
40-
};
24+
return propsToAttributes(headingPropSchema);
4125
},
4226

4327
addInputRules() {

packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
PropSchema,
99
createBlockSpecFromStronglyTypedTiptapNode,
1010
createStronglyTypedTiptapNode,
11+
propsToAttributes,
1112
} from "../../../schema/index.js";
1213
import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers.js";
1314
import { defaultProps } from "../../defaultProps.js";
@@ -24,22 +25,9 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
2425
name: "checkListItem",
2526
content: "inline*",
2627
group: "blockContent",
28+
2729
addAttributes() {
28-
return {
29-
checked: {
30-
default: false,
31-
// instead of "checked" attributes, use "data-checked"
32-
parseHTML: (element) =>
33-
element.getAttribute("data-checked") === "true" || undefined,
34-
renderHTML: (attributes) => {
35-
return attributes.checked
36-
? {
37-
"data-checked": (attributes.checked as boolean).toString(),
38-
}
39-
: {};
40-
},
41-
},
42-
};
30+
return propsToAttributes(checkListItemPropSchema);
4331
},
4432

4533
addInputRules() {

packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListIndexingPlugin.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const NumberedListIndexingPlugin = () => {
2020
node.type.name === "blockContainer" &&
2121
node.firstChild!.type.name === "numberedListItem"
2222
) {
23-
let newIndex = "1";
23+
let newIndex = `${node.firstChild!.attrs["start"] || 1}`;
2424

2525
const blockInfo = getBlockInfo({
2626
posBeforeNode: pos,
@@ -60,13 +60,21 @@ export const NumberedListIndexingPlugin = () => {
6060

6161
const contentNode = blockInfo.blockContent.node;
6262
const index = contentNode.attrs["index"];
63+
const isFirst =
64+
prevBlock?.firstChild?.type.name !== "numberedListItem";
6365

64-
if (index !== newIndex) {
66+
if (index !== newIndex || (contentNode.attrs.start && !isFirst)) {
6567
modified = true;
6668

69+
const { start, ...attrs } = contentNode.attrs;
70+
6771
tr.setNodeMarkup(blockInfo.blockContent.beforePos, undefined, {
68-
...contentNode.attrs,
72+
...attrs,
6973
index: newIndex,
74+
...(typeof start === "number" &&
75+
isFirst && {
76+
start,
77+
}),
7078
});
7179
}
7280
}

packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
PropSchema,
66
createBlockSpecFromStronglyTypedTiptapNode,
77
createStronglyTypedTiptapNode,
8+
propsToAttributes,
89
} from "../../../schema/index.js";
910
import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers.js";
1011
import { defaultProps } from "../../defaultProps.js";
@@ -13,6 +14,7 @@ import { NumberedListIndexingPlugin } from "./NumberedListIndexingPlugin.js";
1314

1415
export const numberedListItemPropSchema = {
1516
...defaultProps,
17+
start: { default: undefined, type: "number" },
1618
} satisfies PropSchema;
1719

1820
const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
@@ -22,6 +24,9 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
2224
priority: 90,
2325
addAttributes() {
2426
return {
27+
...propsToAttributes(numberedListItemPropSchema),
28+
// the index attribute is only used internally (it's not part of the blocknote schema)
29+
// that's why it's defined explicitly here, and not part of the prop schema
2530
index: {
2631
default: null,
2732
parseHTML: (element) => element.getAttribute("data-index"),
@@ -38,15 +43,17 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
3843
return [
3944
// Creates an ordered list when starting with "1.".
4045
new InputRule({
41-
find: new RegExp(`^1\\.\\s$`),
42-
handler: ({ state, chain, range }) => {
46+
find: new RegExp(`^(\\d+)\\.\\s$`),
47+
handler: ({ state, chain, range, match }) => {
4348
const blockInfo = getBlockInfoFromSelection(state);
4449
if (
4550
!blockInfo.isBlockContainer ||
46-
blockInfo.blockContent.node.type.spec.content !== "inline*"
51+
blockInfo.blockContent.node.type.spec.content !== "inline*" ||
52+
blockInfo.blockNoteType === "numberedListItem"
4753
) {
4854
return;
4955
}
56+
const startIndex = parseInt(match[1]);
5057

5158
chain()
5259
.command(
@@ -55,7 +62,11 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
5562
blockInfo.bnBlock.beforePos,
5663
{
5764
type: "numberedListItem",
58-
props: {},
65+
props:
66+
(startIndex === 1 && {}) ||
67+
({
68+
start: startIndex,
69+
} as any),
5970
}
6071
)
6172
)
@@ -116,7 +127,16 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
116127
parent.tagName === "OL" ||
117128
(parent.tagName === "DIV" && parent.parentElement!.tagName === "OL")
118129
) {
119-
return {};
130+
const startIndex =
131+
parseInt(parent.getAttribute("start") || "1") || 1;
132+
133+
if (element.previousSibling || startIndex === 1) {
134+
return {};
135+
}
136+
137+
return {
138+
start: startIndex,
139+
};
120140
}
121141

122142
return false;

packages/core/src/editor/Block.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,13 @@ NESTED BLOCKS
114114
}
115115

116116
/* HEADINGS*/
117-
[data-level="1"] {
117+
[data-content-type="heading"] {
118118
--level: 3em;
119119
}
120-
[data-level="2"] {
120+
[data-content-type="heading"][data-level="2"] {
121121
--level: 2em;
122122
}
123-
[data-level="3"] {
123+
[data-content-type="heading"][data-level="3"] {
124124
--level: 1.3em;
125125
}
126126

0 commit comments

Comments
 (0)