From 27ad370c49e764182ea7785a81bf5b100c87589a Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Fri, 21 Mar 2025 17:20:46 +0100 Subject: [PATCH 1/5] Replaced TipTap hard break extension with our own --- .../core/src/editor/BlockNoteExtensions.ts | 4 +- .../src/extensions/HardBreak/HardBreak.ts | 84 +++++++++++++++++++ .../KeyboardShortcutsExtension.ts | 1 + 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/extensions/HardBreak/HardBreak.ts diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts index 9d7f1fd0fc..a5233f11f6 100644 --- a/packages/core/src/editor/BlockNoteExtensions.ts +++ b/packages/core/src/editor/BlockNoteExtensions.ts @@ -1,6 +1,5 @@ import { AnyExtension, Extension, extensions } from "@tiptap/core"; import { Gapcursor } from "@tiptap/extension-gapcursor"; -import { HardBreak } from "@tiptap/extension-hard-break"; import { History } from "@tiptap/extension-history"; import { Link } from "@tiptap/extension-link"; import { Text } from "@tiptap/extension-text"; @@ -17,6 +16,7 @@ import { CommentsPlugin } from "../extensions/Comments/CommentsPlugin.js"; import type { ThreadStore } from "../comments/index.js"; import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js"; import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js"; +import { HardBreak } from "../extensions/HardBreak/HardBreak.js"; import { KeyboardShortcutsExtension } from "../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js"; import { LinkToolbarProsemirrorPlugin } from "../extensions/LinkToolbar/LinkToolbarPlugin.js"; import { @@ -179,7 +179,7 @@ const getTipTapExtensions = < types: ["blockContainer", "columnList", "column"], setIdAttribute: opts.setIdAttribute, }), - HardBreak.extend({ priority: 10 }), + HardBreak, // Comments, // basics: diff --git a/packages/core/src/extensions/HardBreak/HardBreak.ts b/packages/core/src/extensions/HardBreak/HardBreak.ts new file mode 100644 index 0000000000..54b1d3e3f8 --- /dev/null +++ b/packages/core/src/extensions/HardBreak/HardBreak.ts @@ -0,0 +1,84 @@ +// Stripped down version of the TipTap HardBreak extension: +// https://github.com/ueberdosis/tiptap/blob/f3258d9ee5fb7979102fe63434f6ea4120507311/packages/extension-hard-break/src/hard-break.ts#L80 +// Changes: +// - Removed options +// - Removed keyboard shortcuts & moved them to the `KeyboardShortcutsExtension` +// - Removed `setHardBreak` check if parent node is isolating (this is beacuse +// custom blocks are isolating). +// - Added priority +import { mergeAttributes, Node } from "@tiptap/core"; + +declare module "@tiptap/core" { + interface Commands<ReturnType> { + hardBreak: { + /** + * Add a hard break + * @example editor.commands.setHardBreak() + */ + setHardBreak: () => ReturnType; + }; + } +} + +export const HardBreak = Node.create({ + name: "hardBreak", + + inline: true, + + group: "inline", + + selectable: false, + + linebreakReplacement: true, + + priority: 10, + + parseHTML() { + return [{ tag: "br" }]; + }, + + renderHTML({ HTMLAttributes }) { + return ["br", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, + + renderText() { + return "\n"; + }, + + addCommands() { + return { + setHardBreak: + () => + ({ commands, chain, state, editor }) => { + return commands.first([ + () => commands.exitCode(), + () => + commands.command(() => { + const { selection, storedMarks } = state; + + const { keepMarks } = this.options; + const { splittableMarks } = editor.extensionManager; + const marks = + storedMarks || + (selection.$to.parentOffset && selection.$from.marks()); + + return chain() + .insertContent({ type: this.name }) + .command(({ tr, dispatch }) => { + if (dispatch && marks && keepMarks) { + const filteredMarks = marks.filter((mark) => + splittableMarks.includes(mark.type.name) + ); + + tr.ensureMarks(filteredMarks); + } + + return true; + }) + .run(); + }), + ]); + }, + }; + }, +}); diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index ae00f7074a..dd70ee55dd 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -543,6 +543,7 @@ export const KeyboardShortcutsExtension = Extension.create<{ Backspace: handleBackspace, Delete: handleDelete, Enter: handleEnter, + "Shift-Enter": () => this.editor.commands.setHardBreak(), // Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the // editor since the browser will try to use tab for keyboard navigation. Tab: () => { From 13b90073862d9e6ba753adc15a2521d5e89c1108 Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Tue, 25 Mar 2025 19:05:44 +0100 Subject: [PATCH 2/5] Removed `setHardBreak` command and dependency --- package-lock.json | 13 ----- packages/core/package.json | 1 - .../TableBlockContent/TableExtension.ts | 2 +- .../src/extensions/HardBreak/HardBreak.ts | 53 +------------------ .../KeyboardShortcutsExtension.ts | 3 +- 5 files changed, 5 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87692758d1..c6e9fd50bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10622,18 +10622,6 @@ "@tiptap/pm": "^2.7.0" } }, - "node_modules/@tiptap/extension-hard-break": { - "version": "2.11.5", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.5.tgz", - "integrity": "sha512-q9doeN+Yg9F5QNTG8pZGYfNye3tmntOwch683v0CCVCI4ldKaLZ0jG3NbBTq+mosHYdgOH2rNbIORlRRsQ+iYQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.7.0" - } - }, "node_modules/@tiptap/extension-history": { "version": "2.11.5", "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.5.tgz", @@ -31224,7 +31212,6 @@ "@tiptap/extension-collaboration": "^2.11.5", "@tiptap/extension-collaboration-cursor": "^2.11.5", "@tiptap/extension-gapcursor": "^2.11.5", - "@tiptap/extension-hard-break": "^2.11.5", "@tiptap/extension-history": "^2.11.5", "@tiptap/extension-horizontal-rule": "^2.11.5", "@tiptap/extension-italic": "^2.11.5", diff --git a/packages/core/package.json b/packages/core/package.json index f99e435d17..07183e8c2d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -69,7 +69,6 @@ "@tiptap/extension-collaboration": "^2.11.5", "@tiptap/extension-collaboration-cursor": "^2.11.5", "@tiptap/extension-gapcursor": "^2.11.5", - "@tiptap/extension-hard-break": "^2.11.5", "@tiptap/extension-history": "^2.11.5", "@tiptap/extension-horizontal-rule": "^2.11.5", "@tiptap/extension-italic": "^2.11.5", diff --git a/packages/core/src/blocks/TableBlockContent/TableExtension.ts b/packages/core/src/blocks/TableBlockContent/TableExtension.ts index 33b47c0c90..0ed6836eea 100644 --- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts +++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts @@ -31,7 +31,7 @@ export const TableExtension = Extension.create({ this.editor.state.selection.$head.parent.type.name === "tableParagraph" ) { - this.editor.commands.setHardBreak(); + this.editor.commands.insertContent({ type: "hardBreak" }); return true; } diff --git a/packages/core/src/extensions/HardBreak/HardBreak.ts b/packages/core/src/extensions/HardBreak/HardBreak.ts index 54b1d3e3f8..6c1a99b241 100644 --- a/packages/core/src/extensions/HardBreak/HardBreak.ts +++ b/packages/core/src/extensions/HardBreak/HardBreak.ts @@ -3,23 +3,11 @@ // Changes: // - Removed options // - Removed keyboard shortcuts & moved them to the `KeyboardShortcutsExtension` -// - Removed `setHardBreak` check if parent node is isolating (this is beacuse -// custom blocks are isolating). +// - Removed `setHardBreak` command (added a simpler version in the Shift+Enter +// handler in `KeyboardShortcutsExtension`). // - Added priority import { mergeAttributes, Node } from "@tiptap/core"; -declare module "@tiptap/core" { - interface Commands<ReturnType> { - hardBreak: { - /** - * Add a hard break - * @example editor.commands.setHardBreak() - */ - setHardBreak: () => ReturnType; - }; - } -} - export const HardBreak = Node.create({ name: "hardBreak", @@ -44,41 +32,4 @@ export const HardBreak = Node.create({ renderText() { return "\n"; }, - - addCommands() { - return { - setHardBreak: - () => - ({ commands, chain, state, editor }) => { - return commands.first([ - () => commands.exitCode(), - () => - commands.command(() => { - const { selection, storedMarks } = state; - - const { keepMarks } = this.options; - const { splittableMarks } = editor.extensionManager; - const marks = - storedMarks || - (selection.$to.parentOffset && selection.$from.marks()); - - return chain() - .insertContent({ type: this.name }) - .command(({ tr, dispatch }) => { - if (dispatch && marks && keepMarks) { - const filteredMarks = marks.filter((mark) => - splittableMarks.includes(mark.type.name) - ); - - tr.ensureMarks(filteredMarks); - } - - return true; - }) - .run(); - }), - ]); - }, - }; - }, }); diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index dd70ee55dd..148a7e2579 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -543,7 +543,8 @@ export const KeyboardShortcutsExtension = Extension.create<{ Backspace: handleBackspace, Delete: handleDelete, Enter: handleEnter, - "Shift-Enter": () => this.editor.commands.setHardBreak(), + "Shift-Enter": () => + this.editor.commands.insertContent({ type: "hardBreak" }), // Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the // editor since the browser will try to use tab for keyboard navigation. Tab: () => { From 4246e654f0689c0b37fcf42beab40ba254fd384e Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Wed, 26 Mar 2025 17:06:02 +0100 Subject: [PATCH 3/5] Added hard break shortcut configuration to API --- .../KeyboardShortcutsExtension.ts | 38 +++++++++++++++++-- packages/core/src/schema/blocks/types.ts | 1 + 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index 148a7e2579..4a651e90b4 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -437,8 +437,8 @@ export const KeyboardShortcutsExtension = Extension.create<{ }), ]); - const handleEnter = () => - this.editor.commands.first(({ commands }) => [ + const handleEnter = (withShift = false) => { + return this.editor.commands.first(({ commands }) => [ // Removes a level of nesting if the block is empty & indented, while the selection is also empty & at the start // of the block. () => @@ -467,6 +467,36 @@ export const KeyboardShortcutsExtension = Extension.create<{ return commands.liftListItem("blockContainer"); } + return false; + }), + // Creates a hard break if block is configured to do so. + () => + commands.command(({ state }) => { + const blockInfo = getBlockInfoFromSelection(state); + + const blockHardBreakShortcut = + this.options.editor.schema.blockSchema[blockInfo.blockNoteType] + .hardBreakShortcut; + + if (blockHardBreakShortcut === "none") { + return false; + } + + if ( + // If shortcut is not configured, or is configured as "shift+enter", + // create a hard break for shift+enter, but not for enter. + ((blockHardBreakShortcut === undefined || + blockHardBreakShortcut === "shift+enter") && + withShift) || + // If shortcut is configured as "enter", create a hard break for + // both enter and shift+enter. + blockHardBreakShortcut === "enter" + ) { + return commands.insertContent({ + type: "hardBreak", + }); + } + return false; }), // Creates a new block and moves the selection to it if the current one is empty, while the selection is also @@ -538,11 +568,13 @@ export const KeyboardShortcutsExtension = Extension.create<{ return false; }), ]); + }; return { Backspace: handleBackspace, Delete: handleDelete, - Enter: handleEnter, + Enter: () => handleEnter(), + "Shift-Enter": () => handleEnter(true), "Shift-Enter": () => this.editor.commands.insertContent({ type: "hardBreak" }), // Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index dac8f5c9e9..0db756ec74 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -63,6 +63,7 @@ export type BlockConfig = content: "inline" | "none" | "table"; isSelectable?: boolean; isFileBlock?: false; + hardBreakShortcut?: "shift+enter" | "enter" | "none"; } | FileBlockConfig; From 68882d58b7676aae7ad2f4feac40a156c8cf616e Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Thu, 27 Mar 2025 10:54:56 +0100 Subject: [PATCH 4/5] Fixed lint --- .../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index 4a651e90b4..cdc27e94ef 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -575,8 +575,6 @@ export const KeyboardShortcutsExtension = Extension.create<{ Delete: handleDelete, Enter: () => handleEnter(), "Shift-Enter": () => handleEnter(true), - "Shift-Enter": () => - this.editor.commands.insertContent({ type: "hardBreak" }), // Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the // editor since the browser will try to use tab for keyboard navigation. Tab: () => { From 98716a10297f3b08146285582f1297c4e281d9ee Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Thu, 27 Mar 2025 15:09:30 +0100 Subject: [PATCH 5/5] Small cleanup --- .../KeyboardShortcuts/KeyboardShortcutsExtension.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index cdc27e94ef..9e7d540f64 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -474,9 +474,9 @@ export const KeyboardShortcutsExtension = Extension.create<{ commands.command(({ state }) => { const blockInfo = getBlockInfoFromSelection(state); - const blockHardBreakShortcut = + const blockHardBreakShortcut: "shift+enter" | "enter" | "none" = this.options.editor.schema.blockSchema[blockInfo.blockNoteType] - .hardBreakShortcut; + .hardBreakShortcut ?? "shift+enter"; if (blockHardBreakShortcut === "none") { return false; @@ -485,9 +485,7 @@ export const KeyboardShortcutsExtension = Extension.create<{ if ( // If shortcut is not configured, or is configured as "shift+enter", // create a hard break for shift+enter, but not for enter. - ((blockHardBreakShortcut === undefined || - blockHardBreakShortcut === "shift+enter") && - withShift) || + (blockHardBreakShortcut === "shift+enter" && withShift) || // If shortcut is configured as "enter", create a hard break for // both enter and shift+enter. blockHardBreakShortcut === "enter"