Skip to content

Commit 38d2f74

Browse files
committed
Updated keyboard shortcut
1 parent fcc0d12 commit 38d2f74

File tree

2 files changed

+61
-166
lines changed

2 files changed

+61
-166
lines changed

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

Lines changed: 31 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ResolvedPos, type Node } from "prosemirror-model";
2-
import { TextSelection, type Transaction } from "prosemirror-state";
1+
import { Slice, type Node } from "prosemirror-model";
2+
import { type Transaction } from "prosemirror-state";
33
import { ReplaceAroundStep } from "prosemirror-transform";
44
import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js";
55
import type {
@@ -8,158 +8,9 @@ import type {
88
InlineContentSchema,
99
StyleSchema,
1010
} from "../../../../schema/index.js";
11-
import { getBlockInfoFromResolvedPos } from "../../../getBlockInfoFromPos.js";
1211
import { blockToNode } from "../../../nodeConversions/blockToNode.js";
1312
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
1413
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: 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: 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: 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: 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(columnList.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-
}
16314

16415
/**
16516
* Checks if a `column` node is empty, i.e. if it has only a single empty
@@ -258,9 +109,11 @@ export function fixColumnList(tr: Transaction, columnListPos: number) {
258109
const firstColumnBeforePos = columnListPos + 1;
259110
const $firstColumnBeforePos = tr.doc.resolve(firstColumnBeforePos);
260111
const firstColumn = $firstColumnBeforePos.nodeAfter;
112+
261113
const lastColumnAfterPos = columnListPos + columnList.nodeSize - 1;
262114
const $lastColumnAfterPos = tr.doc.resolve(lastColumnAfterPos);
263115
const lastColumn = $lastColumnAfterPos.nodeBefore;
116+
264117
if (!firstColumn || !lastColumn) {
265118
throw new Error("Invalid columnList: does not have child node.");
266119
}
@@ -269,32 +122,47 @@ export function fixColumnList(tr: Transaction, columnListPos: number) {
269122
const lastColumnEmpty = isEmptyColumn(lastColumn);
270123

271124
if (firstColumnEmpty && lastColumnEmpty) {
125+
// Removes `columnList`
272126
tr.delete(columnListPos, columnListPos + columnList.nodeSize);
273127

274128
return;
275129
}
276130

277131
if (firstColumnEmpty) {
278-
const lastColumnContent = tr.doc.slice(
279-
lastColumnAfterPos - lastColumn.nodeSize + 1,
280-
lastColumnAfterPos - 1,
132+
tr.step(
133+
new ReplaceAroundStep(
134+
// Replaces `columnList`.
135+
columnListPos,
136+
columnListPos + columnList.nodeSize,
137+
// Replaces with content of last `column`.
138+
lastColumnAfterPos - lastColumn.nodeSize + 1,
139+
lastColumnAfterPos - 1,
140+
// Doesn't append anything.
141+
Slice.empty,
142+
0,
143+
false,
144+
),
281145
);
282146

283-
tr.delete(columnListPos, columnListPos + columnList.nodeSize);
284-
tr.insert(columnListPos, lastColumnContent.content);
285-
286147
return;
287148
}
288149

289150
if (lastColumnEmpty) {
290-
const firstColumnContent = tr.doc.slice(
291-
firstColumnBeforePos + 1,
292-
firstColumnBeforePos + firstColumn.nodeSize - 1,
151+
tr.step(
152+
new ReplaceAroundStep(
153+
// Replaces `columnList`.
154+
columnListPos,
155+
columnListPos + columnList.nodeSize,
156+
// Replaces with content of first `column`.
157+
firstColumnBeforePos + 1,
158+
firstColumnBeforePos + firstColumn.nodeSize - 1,
159+
// Doesn't append anything.
160+
Slice.empty,
161+
0,
162+
false,
163+
),
293164
);
294165

295-
tr.delete(columnListPos, columnListPos + columnList.nodeSize);
296-
tr.insert(columnListPos, firstColumnContent.content);
297-
298166
return;
299167
}
300168
}

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { splitBlockCommand } from "../../api/blockManipulation/commands/splitBlo
1111
import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
1212
import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js";
1313
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
14-
import { moveFirstBlockInColumn } from "../../api/blockManipulation/commands/replaceBlocks/replaceBlocks.js";
14+
import { fixColumnList } from "../../api/blockManipulation/commands/replaceBlocks/replaceBlocks.js";
1515

1616
export const KeyboardShortcutsExtension = Extension.create<{
1717
editor: BlockNoteEditor<any, any, any>;
@@ -24,7 +24,7 @@ export const KeyboardShortcutsExtension = Extension.create<{
2424
addKeyboardShortcuts() {
2525
// handleBackspace is partially adapted from https://github.com/ueberdosis/tiptap/blob/ed56337470efb4fd277128ab7ef792b37cfae992/packages/core/src/extensions/keymap.ts
2626
const handleBackspace = () =>
27-
this.editor.commands.first(({ chain, commands }) => [
27+
this.editor.commands.first(({ chain, commands, tr }) => [
2828
// Deletes the selection if it's not empty.
2929
() => commands.deleteSelection(),
3030
// Undoes an input rule if one was triggered in the last editor state change.
@@ -123,8 +123,35 @@ export const KeyboardShortcutsExtension = Extension.create<{
123123
return false;
124124
}
125125

126+
const $blockPos = state.doc.resolve(blockInfo.bnBlock.beforePos);
127+
const $columnPos = state.doc.resolve($blockPos.before(-1));
128+
const columnListPos = $columnPos.before();
129+
126130
if (dispatch) {
127-
moveFirstBlockInColumn(state.tr, $pos);
131+
const fragment = state.doc.slice(
132+
blockInfo.bnBlock.beforePos,
133+
blockInfo.bnBlock.afterPos,
134+
).content;
135+
136+
tr.delete(
137+
blockInfo.bnBlock.beforePos,
138+
blockInfo.bnBlock.afterPos,
139+
);
140+
141+
if ($columnPos.index() === 0) {
142+
// Fix `columnList` and insert the block before it.
143+
fixColumnList(state.tr, columnListPos);
144+
tr.insert(columnListPos, fragment);
145+
tr.setSelection(TextSelection.create(tr.doc, columnListPos));
146+
} else {
147+
// Insert the block at the end of the first column and fix
148+
// `columnList`.
149+
tr.insert($columnPos.pos - 1, fragment);
150+
tr.setSelection(
151+
TextSelection.create(tr.doc, $columnPos.pos - 1),
152+
);
153+
fixColumnList(tr, columnListPos);
154+
}
128155
}
129156

130157
return true;

0 commit comments

Comments
 (0)