Skip to content

Commit e35f026

Browse files
BNSplitBlock fix (#72)
* Fixed block splitting bug * Rewrite to use `Slice` instead of `textContent` for block content * Minor changes * Added related keyboard handler tests * Updated Chromium snapshot * Increased test timeout
1 parent fe3c12b commit e35f026

9 files changed

+685
-15
lines changed

packages/core/src/extensions/Blocks/nodes/BlockContainer.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mergeAttributes, Node } from "@tiptap/core";
2-
import { Slice } from "prosemirror-model";
2+
import { Fragment, Slice } from "prosemirror-model";
33
import { TextSelection } from "prosemirror-state";
44
import { BlockUpdate } from "../apiTypes";
55
import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
@@ -222,20 +222,36 @@ export const BlockContainer = Node.create<IBlock>({
222222
const { contentNode, contentType, startPos, endPos, depth } =
223223
blockInfo;
224224

225-
const newBlockInsertionPos = endPos + 1;
226-
227-
// Creates new block first, otherwise positions get changed due to the original block's content changing.
228-
// Only text content is transferred to the new block.
229-
const secondBlockContent = state.doc.textBetween(posInBlock, endPos);
225+
const originalBlockContent = state.doc.cut(startPos + 1, posInBlock);
226+
const newBlockContent = state.doc.cut(posInBlock, endPos - 1);
230227

231228
const newBlock =
232229
state.schema.nodes["blockContainer"].createAndFill()!;
230+
231+
const newBlockInsertionPos = endPos + 1;
233232
const newBlockContentPos = newBlockInsertionPos + 2;
234233

235234
if (dispatch) {
235+
// Creates a new block. Since the schema requires it to have a content node, a paragraph node is created
236+
// automatically, spanning newBlockContentPos to newBlockContentPos + 1.
236237
state.tr.insert(newBlockInsertionPos, newBlock);
237-
state.tr.insertText(secondBlockContent, newBlockContentPos);
238238

239+
// Replaces the content of the newly created block's content node. Doesn't replace the whole content node so
240+
// its type doesn't change.
241+
state.tr.replace(
242+
newBlockContentPos,
243+
newBlockContentPos + 1,
244+
newBlockContent.content.size > 0
245+
? new Slice(
246+
Fragment.from(newBlockContent),
247+
depth + 2,
248+
depth + 2
249+
)
250+
: undefined
251+
);
252+
253+
// Changes the type of the content node. The range doesn't matter as long as both from and to positions are
254+
// within the content node.
239255
if (keepType) {
240256
state.tr.setBlockType(
241257
newBlockContentPos,
@@ -244,22 +260,30 @@ export const BlockContainer = Node.create<IBlock>({
244260
contentNode.attrs
245261
);
246262
}
247-
}
248263

249-
// Updates content of original block.
250-
const firstBlockContent = state.doc.content.cut(startPos, posInBlock);
264+
// Sets the selection to the start of the new block's content node.
265+
state.tr.setSelection(
266+
new TextSelection(state.doc.resolve(newBlockContentPos))
267+
);
251268

252-
if (dispatch) {
269+
// Replaces the content of the original block's content node. Doesn't replace the whole content node so its
270+
// type doesn't change.
253271
state.tr.replace(
254-
startPos,
255-
endPos,
256-
new Slice(firstBlockContent, depth, depth)
272+
startPos + 1,
273+
endPos - 1,
274+
originalBlockContent.content.size > 0
275+
? new Slice(
276+
Fragment.from(originalBlockContent),
277+
depth + 2,
278+
depth + 2
279+
)
280+
: undefined
257281
);
258282
}
259283

260284
return true;
261285
},
262-
// Changes the content of a block at a given position to a given type.
286+
// Updates the type and attributes of a block at a given position.
263287
BNUpdateBlock:
264288
(posInBlock, blockUpdate) =>
265289
({ state, dispatch }) => {

tests/end-to-end/keyboardhandlers/keyboardhandlers.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test } from "../../setup/setupScript";
22
import {
33
BASE_URL,
4+
ITALIC_BUTTON_SELECTOR,
45
H_ONE_BLOCK_SELECTOR,
56
H_TWO_BLOCK_SELECTOR,
67
} from "../../utils/const";
@@ -34,4 +35,39 @@ test.describe("Check Keyboard Handlers' Behaviour", () => {
3435

3536
await compareDocToSnapshot(page, "enterSelectionNotEmpty.json");
3637
});
38+
test("Check Enter preserves marks", async ({ page }) => {
39+
await focusOnEditor(page);
40+
await insertHeading(page, 1);
41+
42+
const element = await page.locator(H_ONE_BLOCK_SELECTOR);
43+
let boundingBox = await element.boundingBox();
44+
let { x, y, height } = boundingBox;
45+
46+
await page.mouse.click(x + 35, y + height / 2, { clickCount: 2 });
47+
await page.locator(ITALIC_BUTTON_SELECTOR).click();
48+
await page.waitForTimeout(600);
49+
await page.mouse.click(x + 35, y + height / 2);
50+
await page.keyboard.press("Enter");
51+
52+
await page.pause();
53+
54+
await compareDocToSnapshot(page, "enterPreservesMarks.json");
55+
});
56+
test("Check Enter preserves nested blocks", async ({ page }) => {
57+
await focusOnEditor(page);
58+
await insertHeading(page, 1);
59+
await page.keyboard.press("Tab");
60+
await insertHeading(page, 2);
61+
await page.keyboard.press("Tab");
62+
await insertHeading(page, 3);
63+
64+
const element = await page.locator(H_ONE_BLOCK_SELECTOR);
65+
let boundingBox = await element.boundingBox();
66+
let { x, y, height } = boundingBox;
67+
68+
await page.mouse.click(x + 35, y + height / 2);
69+
await page.keyboard.press("Enter");
70+
71+
await compareDocToSnapshot(page, "enterPreservesNestedBlocks.json");
72+
});
3773
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"type": "doc",
3+
"content": [
4+
{
5+
"type": "blockGroup",
6+
"content": [
7+
{
8+
"type": "blockContainer",
9+
"attrs": {
10+
"id": 0,
11+
"textColor": "default",
12+
"backgroundColor": "default"
13+
},
14+
"content": [
15+
{
16+
"type": "heading",
17+
"attrs": {
18+
"textAlignment": "left",
19+
"level": "1"
20+
},
21+
"content": [
22+
{
23+
"type": "text",
24+
"marks": [
25+
{
26+
"type": "italic"
27+
}
28+
],
29+
"text": "H"
30+
}
31+
]
32+
}
33+
]
34+
},
35+
{
36+
"type": "blockContainer",
37+
"attrs": {
38+
"id": 2,
39+
"textColor": "default",
40+
"backgroundColor": "default"
41+
},
42+
"content": [
43+
{
44+
"type": "paragraph",
45+
"attrs": {
46+
"textAlignment": "left"
47+
},
48+
"content": [
49+
{
50+
"type": "text",
51+
"marks": [
52+
{
53+
"type": "italic"
54+
}
55+
],
56+
"text": "eading"
57+
}
58+
]
59+
}
60+
]
61+
},
62+
{
63+
"type": "blockContainer",
64+
"attrs": {
65+
"id": 1,
66+
"textColor": "default",
67+
"backgroundColor": "default"
68+
},
69+
"content": [
70+
{
71+
"type": "paragraph",
72+
"attrs": {
73+
"textAlignment": "left"
74+
}
75+
}
76+
]
77+
}
78+
]
79+
}
80+
]
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"type": "doc",
3+
"content": [
4+
{
5+
"type": "blockGroup",
6+
"content": [
7+
{
8+
"type": "blockContainer",
9+
"attrs": {
10+
"id": 0,
11+
"textColor": "default",
12+
"backgroundColor": "default"
13+
},
14+
"content": [
15+
{
16+
"type": "heading",
17+
"attrs": {
18+
"textAlignment": "left",
19+
"level": "1"
20+
},
21+
"content": [
22+
{
23+
"type": "text",
24+
"marks": [
25+
{
26+
"type": "italic"
27+
}
28+
],
29+
"text": "H"
30+
}
31+
]
32+
}
33+
]
34+
},
35+
{
36+
"type": "blockContainer",
37+
"attrs": {
38+
"id": 2,
39+
"textColor": "default",
40+
"backgroundColor": "default"
41+
},
42+
"content": [
43+
{
44+
"type": "paragraph",
45+
"attrs": {
46+
"textAlignment": "left"
47+
},
48+
"content": [
49+
{
50+
"type": "text",
51+
"marks": [
52+
{
53+
"type": "italic"
54+
}
55+
],
56+
"text": "eading"
57+
}
58+
]
59+
}
60+
]
61+
},
62+
{
63+
"type": "blockContainer",
64+
"attrs": {
65+
"id": 1,
66+
"textColor": "default",
67+
"backgroundColor": "default"
68+
},
69+
"content": [
70+
{
71+
"type": "paragraph",
72+
"attrs": {
73+
"textAlignment": "left"
74+
}
75+
}
76+
]
77+
}
78+
]
79+
}
80+
]
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"type": "doc",
3+
"content": [
4+
{
5+
"type": "blockGroup",
6+
"content": [
7+
{
8+
"type": "blockContainer",
9+
"attrs": {
10+
"id": 0,
11+
"textColor": "default",
12+
"backgroundColor": "default"
13+
},
14+
"content": [
15+
{
16+
"type": "heading",
17+
"attrs": {
18+
"textAlignment": "left",
19+
"level": "1"
20+
},
21+
"content": [
22+
{
23+
"type": "text",
24+
"marks": [
25+
{
26+
"type": "italic"
27+
}
28+
],
29+
"text": "H"
30+
}
31+
]
32+
}
33+
]
34+
},
35+
{
36+
"type": "blockContainer",
37+
"attrs": {
38+
"id": 2,
39+
"textColor": "default",
40+
"backgroundColor": "default"
41+
},
42+
"content": [
43+
{
44+
"type": "paragraph",
45+
"attrs": {
46+
"textAlignment": "left"
47+
},
48+
"content": [
49+
{
50+
"type": "text",
51+
"marks": [
52+
{
53+
"type": "italic"
54+
}
55+
],
56+
"text": "eading"
57+
}
58+
]
59+
}
60+
]
61+
},
62+
{
63+
"type": "blockContainer",
64+
"attrs": {
65+
"id": 1,
66+
"textColor": "default",
67+
"backgroundColor": "default"
68+
},
69+
"content": [
70+
{
71+
"type": "paragraph",
72+
"attrs": {
73+
"textAlignment": "left"
74+
}
75+
}
76+
]
77+
}
78+
]
79+
}
80+
]
81+
}

0 commit comments

Comments
 (0)