Skip to content

Commit bd6519b

Browse files
committed
Fixed deleting last block in column not collapsing column/column list
1 parent fe8fb9f commit bd6519b

File tree

2 files changed

+178
-141
lines changed

2 files changed

+178
-141
lines changed

packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts

Lines changed: 171 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,165 @@
1-
import type { Node } from "prosemirror-model";
2-
import type { Transaction } from "prosemirror-state";
1+
import { ResolvedPos, Slice, type Node } from "prosemirror-model";
2+
import { TextSelection, type Transaction } from "prosemirror-state";
3+
import { ReplaceAroundStep } from "prosemirror-transform";
34
import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js";
45
import type {
56
BlockIdentifier,
67
BlockSchema,
78
InlineContentSchema,
89
StyleSchema,
910
} from "../../../../schema/index.js";
11+
import { getBlockInfoFromResolvedPos } from "../../../getBlockInfoFromPos.js";
1012
import { blockToNode } from "../../../nodeConversions/blockToNode.js";
1113
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
1214
import { getPmSchema } from "../../../pmUtil.js";
15+
import {
16+
getParentBlockInfo,
17+
getPrevBlockInfo,
18+
} from "../mergeBlocks/mergeBlocks.js";
19+
20+
// TODO: Where should this function go?
21+
/**
22+
* Moves the first block in a column to the previous/next column and handles
23+
* all necessary collapsing of `column`/`columnList` nodes. Only moves the
24+
* block to the start of the next column if it's in the first column.
25+
* Otherwise, moves the block to the end of the previous column.
26+
* @param tr The transaction to apply changes to.
27+
* @param blockBeforePos The position just before the first block in the column.
28+
* @returns The position just before the block, after it's moved.
29+
*/
30+
export function moveFirstBlockInColumn(
31+
tr: Transaction,
32+
blockBeforePos: ResolvedPos,
33+
): ResolvedPos {
34+
const blockInfo = getBlockInfoFromResolvedPos(blockBeforePos);
35+
if (!blockInfo.isBlockContainer) {
36+
throw new Error(
37+
"Invalid blockBeforePos passed to moveFirstBlockInColumn: does not point to blockContainer node.",
38+
);
39+
}
40+
41+
const prevBlockInfo = getPrevBlockInfo(tr.doc, blockInfo.bnBlock.beforePos);
42+
if (prevBlockInfo) {
43+
throw new Error(
44+
"Invalid blockBeforePos passed to moveFirstBlockInColumn: does not point to first blockContainer node in column.",
45+
);
46+
}
47+
48+
const parentBlockInfo = getParentBlockInfo(
49+
tr.doc,
50+
blockInfo.bnBlock.beforePos,
51+
);
52+
if (parentBlockInfo?.blockNoteType !== "column") {
53+
throw new Error(
54+
"Invalid blockBeforePos passed to moveFirstBlockInColumn: blockContainer node is not child of column.",
55+
);
56+
}
57+
58+
const column = parentBlockInfo;
59+
const columnList = getParentBlockInfo(tr.doc, column.bnBlock.beforePos);
60+
if (columnList?.blockNoteType !== "columnList") {
61+
throw new Error(
62+
"Invalid blockBeforePos passed to moveFirstBlockInColumn: blockContainer node is child of column, but column is not child of columnList node.",
63+
);
64+
}
65+
66+
const shouldRemoveColumn = column.childContainer!.node.childCount === 1;
67+
68+
const shouldRemoveColumnList =
69+
shouldRemoveColumn && columnList.childContainer!.node.childCount === 2;
70+
71+
const isFirstColumn =
72+
columnList.childContainer!.node.firstChild === column.bnBlock.node;
73+
74+
const blockToMove = tr.doc.slice(
75+
blockInfo.bnBlock.beforePos,
76+
blockInfo.bnBlock.afterPos,
77+
false,
78+
);
79+
80+
/*
81+
There are 3 different cases:
82+
a) remove entire column list (if no columns would be remaining)
83+
b) remove just a column (if no blocks inside a column would be remaining)
84+
c) keep columns (if there are blocks remaining inside a column)
85+
86+
Each of these 3 cases has 2 sub-cases, depending on whether the backspace happens at the start of the first (most-left) column,
87+
or at the start of a non-first column.
88+
*/
89+
if (shouldRemoveColumnList) {
90+
if (isFirstColumn) {
91+
tr.step(
92+
new ReplaceAroundStep(
93+
// replace entire column list
94+
columnList.bnBlock.beforePos,
95+
columnList.bnBlock.afterPos,
96+
// select content of remaining column:
97+
column.bnBlock.afterPos + 1,
98+
columnList.bnBlock.afterPos - 2,
99+
blockToMove,
100+
blockToMove.size, // append existing content to blockToMove
101+
false,
102+
),
103+
);
104+
const pos = tr.doc.resolve(column.bnBlock.beforePos);
105+
tr.setSelection(TextSelection.between(pos, pos));
106+
107+
return pos;
108+
} else {
109+
// replaces the column list with the blockToMove slice, prepended with the content of the remaining column
110+
tr.step(
111+
new ReplaceAroundStep(
112+
// replace entire column list
113+
columnList.bnBlock.beforePos,
114+
columnList.bnBlock.afterPos,
115+
// select content of existing column:
116+
columnList.bnBlock.beforePos + 2,
117+
column.bnBlock.beforePos - 1,
118+
blockToMove,
119+
0, // prepend existing content to blockToMove
120+
false,
121+
),
122+
);
123+
const pos = tr.doc.resolve(tr.mapping.map(column.bnBlock.beforePos - 1));
124+
tr.setSelection(TextSelection.between(pos, pos));
125+
126+
return pos;
127+
}
128+
} else if (shouldRemoveColumn) {
129+
if (isFirstColumn) {
130+
// delete column
131+
tr.delete(column.bnBlock.beforePos, column.bnBlock.afterPos);
132+
133+
// move before columnlist
134+
tr.insert(columnList.bnBlock.beforePos, blockToMove.content);
135+
136+
const pos = tr.doc.resolve(columnList.bnBlock.beforePos);
137+
tr.setSelection(TextSelection.between(pos, pos));
138+
139+
return pos;
140+
} else {
141+
// just delete the </column><column> closing and opening tags to merge the columns
142+
tr.delete(column.bnBlock.beforePos - 1, column.bnBlock.beforePos + 1);
143+
const pos = tr.doc.resolve(column.bnBlock.beforePos - 1);
144+
145+
return pos;
146+
}
147+
} else {
148+
// delete block
149+
tr.delete(blockInfo.bnBlock.beforePos, blockInfo.bnBlock.afterPos);
150+
if (isFirstColumn) {
151+
// move before columnlist
152+
tr.insert(columnList.bnBlock.beforePos - 1, blockToMove.content);
153+
} else {
154+
// append block to previous column
155+
tr.insert(column.bnBlock.beforePos - 1, blockToMove.content);
156+
}
157+
const pos = tr.doc.resolve(column.bnBlock.beforePos - 1);
158+
tr.setSelection(TextSelection.between(pos, pos));
159+
160+
return pos;
161+
}
162+
}
13163

14164
export function removeAndInsertBlocks<
15165
BSchema extends BlockSchema,
@@ -70,15 +220,29 @@ export function removeAndInsertBlocks<
70220
}
71221

72222
const oldDocSize = tr.doc.nodeSize;
73-
// Checks if the block is the only child of its parent. In this case, we
74-
// need to delete the parent `blockGroup` node instead of just the
75-
// `blockContainer`.
223+
76224
const $pos = tr.doc.resolve(pos - removedSize);
77-
if (
225+
if ($pos.node().type.name === "column" && $pos.node().childCount === 1) {
226+
// Checks if the block is the only child of a parent `column` node. In
227+
// this case, we need to collapse the `column` or parent `columnList`,
228+
// depending on if the `columnList` has more than 2 children. This is
229+
// handled by `moveFirstBlockInColumn`.
230+
const $newPos = moveFirstBlockInColumn(tr, $pos);
231+
// Instead of deleting it, `moveFirstBlockInColumn` moves the block in
232+
// order to handle the columns after, so we have to delete it manually.
233+
tr.replace(
234+
$newPos.pos,
235+
$newPos.pos + $newPos.nodeAfter!.nodeSize,
236+
Slice.empty,
237+
);
238+
} else if (
78239
$pos.node().type.name === "blockGroup" &&
79240
$pos.node($pos.depth - 1).type.name !== "doc" &&
80241
$pos.node().childCount === 1
81242
) {
243+
// Checks if the block is the only child of a parent `blockGroup` node.
244+
// In this case, we need to delete the parent `blockGroup` node instead
245+
// of just the `blockContainer`.
82246
tr.delete($pos.before(), $pos.after());
83247
} else {
84248
tr.delete(pos - removedSize, pos - removedSize + node.nodeSize);
@@ -89,7 +253,7 @@ export function removeAndInsertBlocks<
89253
return false;
90254
});
91255

92-
// Throws an error if now all blocks could be found.
256+
// Throws an error if not all blocks could be found.
93257
if (idsOfBlocksToRemove.size > 0) {
94258
const notFoundIds = [...idsOfBlocksToRemove].join("\n");
95259

packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts

Lines changed: 7 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { Extension } from "@tiptap/core";
22

33
import { TextSelection } from "prosemirror-state";
4-
import { ReplaceAroundStep } from "prosemirror-transform";
54
import {
65
getBottomNestedBlockInfo,
7-
getParentBlockInfo,
86
getPrevBlockInfo,
97
mergeBlocksCommand,
108
} from "../../api/blockManipulation/commands/mergeBlocks/mergeBlocks.js";
@@ -13,6 +11,7 @@ import { splitBlockCommand } from "../../api/blockManipulation/commands/splitBlo
1311
import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
1412
import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js";
1513
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
14+
import { moveFirstBlockInColumn } from "../../api/blockManipulation/commands/replaceBlocks/replaceBlocks.js";
1615

1716
export const KeyboardShortcutsExtension = Extension.create<{
1817
editor: BlockNoteEditor<any, any, any>;
@@ -107,151 +106,25 @@ export const KeyboardShortcutsExtension = Extension.create<{
107106

108107
const selectionAtBlockStart =
109108
state.selection.from === blockInfo.blockContent.beforePos + 1;
110-
111109
if (!selectionAtBlockStart) {
112110
return false;
113111
}
114112

115-
const prevBlockInfo = getPrevBlockInfo(
116-
state.doc,
117-
blockInfo.bnBlock.beforePos,
118-
);
113+
const $pos = state.doc.resolve(blockInfo.bnBlock.beforePos);
119114

120-
if (prevBlockInfo) {
115+
const prevBlock = $pos.nodeBefore;
116+
if (prevBlock) {
121117
// should be no previous block
122118
return false;
123119
}
124120

125-
const parentBlockInfo = getParentBlockInfo(
126-
state.doc,
127-
blockInfo.bnBlock.beforePos,
128-
);
129-
130-
if (parentBlockInfo?.blockNoteType !== "column") {
121+
const parentBlock = $pos.node();
122+
if (parentBlock.type.name !== "column") {
131123
return false;
132124
}
133125

134-
const column = parentBlockInfo;
135-
136-
const columnList = getParentBlockInfo(
137-
state.doc,
138-
column.bnBlock.beforePos,
139-
);
140-
if (columnList?.blockNoteType !== "columnList") {
141-
throw new Error("parent of column is not a column list");
142-
}
143-
144-
const shouldRemoveColumn =
145-
column.childContainer!.node.childCount === 1;
146-
147-
const shouldRemoveColumnList =
148-
shouldRemoveColumn &&
149-
columnList.childContainer!.node.childCount === 2;
150-
151-
const isFirstColumn =
152-
columnList.childContainer!.node.firstChild ===
153-
column.bnBlock.node;
154-
155126
if (dispatch) {
156-
const blockToMove = state.doc.slice(
157-
blockInfo.bnBlock.beforePos,
158-
blockInfo.bnBlock.afterPos,
159-
false,
160-
);
161-
162-
/*
163-
There are 3 different cases:
164-
a) remove entire column list (if no columns would be remaining)
165-
b) remove just a column (if no blocks inside a column would be remaining)
166-
c) keep columns (if there are blocks remaining inside a column)
167-
168-
Each of these 3 cases has 2 sub-cases, depending on whether the backspace happens at the start of the first (most-left) column,
169-
or at the start of a non-first column.
170-
*/
171-
if (shouldRemoveColumnList) {
172-
if (isFirstColumn) {
173-
state.tr.step(
174-
new ReplaceAroundStep(
175-
// replace entire column list
176-
columnList.bnBlock.beforePos,
177-
columnList.bnBlock.afterPos,
178-
// select content of remaining column:
179-
column.bnBlock.afterPos + 1,
180-
columnList.bnBlock.afterPos - 2,
181-
blockToMove,
182-
blockToMove.size, // append existing content to blockToMove
183-
false,
184-
),
185-
);
186-
const pos = state.tr.doc.resolve(column.bnBlock.beforePos);
187-
state.tr.setSelection(TextSelection.between(pos, pos));
188-
} else {
189-
// replaces the column list with the blockToMove slice, prepended with the content of the remaining column
190-
state.tr.step(
191-
new ReplaceAroundStep(
192-
// replace entire column list
193-
columnList.bnBlock.beforePos,
194-
columnList.bnBlock.afterPos,
195-
// select content of existing column:
196-
columnList.bnBlock.beforePos + 2,
197-
column.bnBlock.beforePos - 1,
198-
blockToMove,
199-
0, // prepend existing content to blockToMove
200-
false,
201-
),
202-
);
203-
const pos = state.tr.doc.resolve(
204-
state.tr.mapping.map(column.bnBlock.beforePos - 1),
205-
);
206-
state.tr.setSelection(TextSelection.between(pos, pos));
207-
}
208-
} else if (shouldRemoveColumn) {
209-
if (isFirstColumn) {
210-
// delete column
211-
state.tr.delete(
212-
column.bnBlock.beforePos,
213-
column.bnBlock.afterPos,
214-
);
215-
216-
// move before columnlist
217-
state.tr.insert(
218-
columnList.bnBlock.beforePos,
219-
blockToMove.content,
220-
);
221-
222-
const pos = state.tr.doc.resolve(
223-
columnList.bnBlock.beforePos,
224-
);
225-
state.tr.setSelection(TextSelection.between(pos, pos));
226-
} else {
227-
// just delete the </column><column> closing and opening tags to merge the columns
228-
state.tr.delete(
229-
column.bnBlock.beforePos - 1,
230-
column.bnBlock.beforePos + 1,
231-
);
232-
}
233-
} else {
234-
// delete block
235-
state.tr.delete(
236-
blockInfo.bnBlock.beforePos,
237-
blockInfo.bnBlock.afterPos,
238-
);
239-
if (isFirstColumn) {
240-
// move before columnlist
241-
state.tr.insert(
242-
columnList.bnBlock.beforePos - 1,
243-
blockToMove.content,
244-
);
245-
} else {
246-
// append block to previous column
247-
state.tr.insert(
248-
column.bnBlock.beforePos - 1,
249-
blockToMove.content,
250-
);
251-
}
252-
const pos = state.tr.doc.resolve(column.bnBlock.beforePos - 1);
253-
state.tr.setSelection(TextSelection.between(pos, pos));
254-
}
127+
moveFirstBlockInColumn(state.tr, $pos);
255128
}
256129

257130
return true;

0 commit comments

Comments
 (0)