Skip to content

Commit 6e2bc11

Browse files
committed
Removed config, made "merge" behavior default
1 parent 6fc70c4 commit 6e2bc11

4 files changed

Lines changed: 124 additions & 98 deletions

File tree

‎packages/core/src/editor/BlockNoteEditor.ts‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -266,18 +266,6 @@ export interface BlockNoteEditorOptions<
266266
*/
267267
tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
268268

269-
/**
270-
* Determines behavior when pressing Backspace at the start of a nested paragraph.
271-
* - `"unindent"`: Unindents the block before allowing it to merge backward.
272-
* - `"merge"`: Skips unindenting and uses the normal backward merge behavior.
273-
* First children merge into their parent; empty first children are removed.
274-
* Falls back to unindenting if neither merging nor deletion is possible.
275-
* Applies to both empty and nonempty blocks. Other block types are still
276-
* converted to paragraphs first, and Shift-Tab always unindents.
277-
* @default "unindent"
278-
*/
279-
backspaceBehavior?: "unindent" | "merge";
280-
281269
/**
282270
* Allows enabling / disabling features of tables.
283271
*

‎packages/core/src/editor/managers/ExtensionManager/extensions.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ export function getDefaultTiptapExtensions(
107107
KeyboardShortcutsExtension.configure({
108108
editor: editor,
109109
tabBehavior: options.tabBehavior,
110-
backspaceBehavior: options.backspaceBehavior ?? "unindent",
111110
}),
112111
BlockGroup.configure({
113112
domAttributes: options.domAttributes,

‎packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.test.ts‎

Lines changed: 119 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ describe("KeyboardShortcutsExtension hardBreakShortcut", () => {
210210
});
211211
});
212212

213-
describe("KeyboardShortcutsExtension backspaceBehavior", () => {
213+
describe("KeyboardShortcutsExtension Backspace", () => {
214214
let editor: BlockNoteEditor | undefined;
215215

216216
afterEach(() => {
@@ -219,7 +219,6 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
219219
});
220220

221221
function createEditor(
222-
backspaceBehavior: "unindent" | "merge" | undefined,
223222
content: string,
224223
type:
225224
| "paragraph"
@@ -228,7 +227,6 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
228227
| "checkListItem" = "paragraph",
229228
) {
230229
const instance = BlockNoteEditor.create({
231-
backspaceBehavior,
232230
trailingBlock: false,
233231
initialContent: [
234232
{
@@ -276,20 +274,8 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
276274
}
277275

278276
describe.each(["", "Current"])("Backspace with content %j", (content) => {
279-
it.each([undefined, "unindent"] as const)(
280-
"unindents with backspaceBehavior=%s",
281-
(behavior) => {
282-
const instance = createEditor(behavior, content);
283-
pressKey(instance, "Backspace");
284-
expectUnindented(instance);
285-
expect(instance.getBlock("current")?.content).toEqual(
286-
content ? [{ type: "text", text: content, styles: {} }] : [],
287-
);
288-
},
289-
);
290-
291277
it("merges without changing following siblings' nesting", () => {
292-
const instance = createEditor("merge", content);
278+
const instance = createEditor(content);
293279
pressKey(instance, "Backspace");
294280

295281
expect(instance.document.map((block) => block.id)).toEqual(["parent"]);
@@ -304,16 +290,16 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
304290
});
305291
});
306292

307-
it("still unindents with Shift-Tab in merge mode", () => {
308-
const instance = createEditor("merge", "Current");
293+
it("still unindents with Shift-Tab", () => {
294+
const instance = createEditor("Current");
309295
pressKey(instance, "Shift-Tab");
310296
expectUnindented(instance);
311297
});
312298

313299
it.each(["bulletListItem", "numberedListItem", "checkListItem"] as const)(
314300
"converts %s to a paragraph before merging",
315301
(type) => {
316-
const instance = createEditor("merge", "Current", type);
302+
const instance = createEditor("Current", type);
317303
pressKey(instance, "Backspace");
318304
expect(instance.getBlock("current")?.type).toBe("paragraph");
319305
expect(
@@ -328,7 +314,7 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
328314
);
329315

330316
it("does not merge when the cursor is inside the block", () => {
331-
const instance = createEditor("merge", "Current");
317+
const instance = createEditor("Current");
332318
instance.setTextCursorPosition("current", "end");
333319
// The shortcut must leave normal character deletion to the browser.
334320
pressKey(instance, "Backspace");
@@ -341,7 +327,7 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
341327
});
342328

343329
function createScenario(initialContent: PartialBlock[]) {
344-
const instance = createEditor("merge", "");
330+
const instance = createEditor("");
345331
instance.replaceBlocks(instance.document, initialContent);
346332
instance.setTextCursorPosition("current", "start");
347333
return instance;
@@ -750,7 +736,7 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
750736
});
751737

752738
it("deletes selected text before attempting to merge or unindent", () => {
753-
const instance = createEditor("merge", "Current");
739+
const instance = createEditor("Current");
754740
const start = instance.prosemirrorView.state.selection.from;
755741
instance.prosemirrorView.dispatch(
756742
instance.prosemirrorView.state.tr.setSelection(
@@ -770,21 +756,18 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
770756
).toEqual(["before", "current", "after"]);
771757
});
772758

773-
it.each(["unindent", "merge"] as const)(
774-
"Delete still merges forward with backspaceBehavior=%s",
775-
(behavior) => {
776-
const instance = createEditor(behavior, "Current");
777-
instance.setTextCursorPosition("before", "end");
778-
pressKey(instance, "Delete");
779-
expect(instance.getBlock("current")).toBeUndefined();
780-
expect(instance.getBlock("before")?.content).toEqual([
781-
{ type: "text", text: "BeforeCurrent", styles: {} },
782-
]);
783-
expect(
784-
instance.getBlock("parent")?.children.map((block) => block.id),
785-
).toEqual(["before", "after"]);
786-
},
787-
);
759+
it("Delete still merges forward", () => {
760+
const instance = createEditor("Current");
761+
instance.setTextCursorPosition("before", "end");
762+
pressKey(instance, "Delete");
763+
expect(instance.getBlock("current")).toBeUndefined();
764+
expect(instance.getBlock("before")?.content).toEqual([
765+
{ type: "text", text: "BeforeCurrent", styles: {} },
766+
]);
767+
expect(
768+
instance.getBlock("parent")?.children.map((block) => block.id),
769+
).toEqual(["before", "after"]);
770+
});
788771

789772
it.each(["image", "table"] as const)(
790773
"promotes descendants of an empty only child under %s and supports undo",
@@ -894,4 +877,103 @@ describe("KeyboardShortcutsExtension backspaceBehavior", () => {
894877
]);
895878
expect(instance.getBlock("current")).toBeUndefined();
896879
});
880+
881+
it.each([false, true])(
882+
"matches PR examples 2 and 4 (previous block has children: %s)",
883+
(nestedTarget) => {
884+
const instance = createScenario([
885+
{
886+
id: "parent",
887+
content: "Parent",
888+
children: nestedTarget ? [{ id: "target", content: "Target" }] : [],
889+
},
890+
{
891+
id: "current",
892+
content: "Current",
893+
children: [{ id: "child", content: "Child" }],
894+
},
895+
]);
896+
pressKey(instance, "Backspace");
897+
expect(instance.document.map((block) => block.id)).toEqual([
898+
"parent",
899+
"child",
900+
]);
901+
const target = nestedTarget ? "target" : "parent";
902+
expect(instance.getBlock(target)?.content).toEqual([
903+
{
904+
type: "text",
905+
text: `${nestedTarget ? "Target" : "Parent"}Current`,
906+
styles: {},
907+
},
908+
]);
909+
expect(instance.getTextCursorPosition().block.id).toBe(target);
910+
expect(instance.prosemirrorView.state.selection.$from.parentOffset).toBe(
911+
6,
912+
);
913+
expect(instance.getBlock("child")?.content).toEqual([
914+
{ type: "text", text: "Child", styles: {} },
915+
]);
916+
},
917+
);
918+
919+
it("deletes a selected nested image", () => {
920+
const instance = createScenario([
921+
{
922+
id: "parent",
923+
content: "Parent",
924+
children: [
925+
{ id: "current", type: "image" },
926+
{ id: "after", content: "After" },
927+
],
928+
},
929+
]);
930+
const info = getBlockInfoFromSelection(instance.prosemirrorView.state);
931+
if (!info.isBlockContainer) {
932+
throw new Error("Expected image block");
933+
}
934+
instance.prosemirrorView.dispatch(
935+
instance.prosemirrorView.state.tr.setSelection(
936+
NodeSelection.create(
937+
instance.prosemirrorView.state.doc,
938+
info.blockContent.beforePos,
939+
),
940+
),
941+
);
942+
pressKey(instance, "Backspace");
943+
expect(instance.getBlock("current")).toBeUndefined();
944+
expect(
945+
instance.getBlock("parent")?.children.map((block) => block.id),
946+
).toEqual(["after"]);
947+
});
948+
949+
it("does nothing at the start of a nested table cell", () => {
950+
const instance = createScenario([
951+
{
952+
id: "parent",
953+
content: "Parent",
954+
children: [
955+
{ id: "current", ...parentSpec("table") },
956+
{ id: "after", content: "After" },
957+
],
958+
},
959+
]);
960+
let pos: number | undefined;
961+
instance.prosemirrorView.state.doc.descendants((node, nodePos) => {
962+
if (node.isText && node.text === "First") {
963+
pos = nodePos;
964+
}
965+
});
966+
if (pos === undefined) {
967+
throw new Error("Expected table cell text");
968+
}
969+
instance.prosemirrorView.dispatch(
970+
instance.prosemirrorView.state.tr.setSelection(
971+
TextSelection.create(instance.prosemirrorView.state.doc, pos),
972+
),
973+
);
974+
const before = instance.document;
975+
pressKey(instance, "Backspace");
976+
expect(instance.document).toEqual(before);
977+
expect(instance.prosemirrorView.state.selection.from).toBe(pos);
978+
});
897979
});

‎packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts‎

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { FormattingToolbarExtension } from "../../FormattingToolbar/FormattingTo
2828
export const KeyboardShortcutsExtension = Extension.create<{
2929
editor: BlockNoteEditor<any, any, any>;
3030
tabBehavior: "prefer-navigate-ui" | "prefer-indent";
31-
backspaceBehavior: "unindent" | "merge";
3231
}>({
3332
priority: 50,
3433

@@ -66,35 +65,8 @@ export const KeyboardShortcutsExtension = Extension.create<{
6665

6766
return false;
6867
}),
69-
// Removes a level of nesting if the block is indented if the selection is at the start of the block.
70-
() =>
71-
commands.command(({ state, tr }) => {
72-
if (this.options.backspaceBehavior === "merge") {
73-
return false;
74-
}
75-
76-
const blockInfo = getBlockInfoFromSelection(state);
77-
if (!blockInfo.isBlockContainer) {
78-
return false;
79-
}
80-
const { blockContent } = blockInfo;
81-
82-
const selectionAtBlockStart =
83-
state.selection.from === blockContent.beforePos + 1;
84-
85-
if (selectionAtBlockStart) {
86-
return liftItem(
87-
tr,
88-
tr.doc.type.schema.nodes["blockContainer"],
89-
tr.doc.type.schema.nodes["blockGroup"],
90-
);
91-
}
92-
93-
return false;
94-
}),
95-
// Merges with the previous block at the start of the block. Nested blocks
96-
// reach this step when backspaceBehavior is "merge". The target must
97-
// contain inline content.
68+
// Merges at the start of the block, into the preceding sibling
69+
// (or its deepest descendant) or parent. Both must have inline content.
9870
() =>
9971
commands.command(({ state }) => {
10072
const blockInfo = getBlockInfoFromSelection(state);
@@ -103,19 +75,6 @@ export const KeyboardShortcutsExtension = Extension.create<{
10375
}
10476
const { bnBlock: blockContainer, blockContent } = blockInfo;
10577

106-
if (this.options.backspaceBehavior !== "merge") {
107-
const prevBlockInfo = getPrevBlockInfo(
108-
state.doc,
109-
blockContainer.beforePos,
110-
);
111-
if (
112-
!prevBlockInfo?.isBlockContainer ||
113-
prevBlockInfo.blockContent.node.type.spec.content !== "inline*"
114-
) {
115-
return false;
116-
}
117-
}
118-
11978
const selectionAtBlockStart =
12079
state.selection.from === blockContent.beforePos + 1;
12180
const selectionEmpty = state.selection.empty;
@@ -243,10 +202,9 @@ export const KeyboardShortcutsExtension = Extension.create<{
243202
state.doc,
244203
blockInfo.bnBlock.beforePos,
245204
);
246-
const parent =
247-
!prevBlockInfo && this.options.backspaceBehavior === "merge"
248-
? getParentBlockInfo(state.doc, blockInfo.bnBlock.beforePos)
249-
: undefined;
205+
const parent = !prevBlockInfo
206+
? getParentBlockInfo(state.doc, blockInfo.bnBlock.beforePos)
207+
: undefined;
250208
const target = prevBlockInfo
251209
? getBottomNestedBlockInfo(state.doc, prevBlockInfo)
252210
: parent;
@@ -347,7 +305,6 @@ export const KeyboardShortcutsExtension = Extension.create<{
347305
commands.command(({ state, tr }) => {
348306
const blockInfo = getBlockInfoFromSelection(state);
349307
if (
350-
this.options.backspaceBehavior !== "merge" ||
351308
!blockInfo.isBlockContainer ||
352309
!state.selection.empty ||
353310
state.selection.from !== blockInfo.blockContent.beforePos + 1

0 commit comments

Comments
 (0)