Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
fcdbf18
feat: wip
nperez0111 Jul 28, 2025
e1bb45e
feat: topological sorting, input rules and keyboard shortcuts
nperez0111 Jul 30, 2025
6b10846
feat: move code block over too
nperez0111 Jul 31, 2025
92952e7
feat: `getContent` parsing for custom blocks
nperez0111 Aug 1, 2025
6d85ef5
fix: render toggle lists
nperez0111 Aug 1, 2025
ec77940
fix: toggle headings
nperez0111 Aug 1, 2025
809ea0a
refactor: custom schema, which depends on no blocks
nperez0111 Aug 1, 2025
b46fcc6
refactor: trying to fit it in
nperez0111 Aug 4, 2025
5751974
fix: get tables working
nperez0111 Aug 4, 2025
c51767a
Made type guards more generic
matthewlipski Aug 5, 2025
97295ce
Updated type guard definitions
matthewlipski Aug 6, 2025
1ff7474
Small fix
matthewlipski Aug 6, 2025
9918510
feat: getting somewhere
nperez0111 Aug 6, 2025
1e0e059
Merge branch 'default-blocks-type-guards' into default-blocks
nperez0111 Aug 6, 2025
fc9e452
Removed `checkDefaultInlineContentTypeInSchema`
matthewlipski Aug 6, 2025
a0fc6e2
Small fixes
matthewlipski Aug 6, 2025
cda8eb6
fix: better parsing for headings
nperez0111 Aug 7, 2025
55957cd
chore: minor fix
nperez0111 Aug 8, 2025
dba7321
fix: widen type?
nperez0111 Aug 8, 2025
760be84
Merge branch 'default-blocks-type-guards' into default-blocks
nperez0111 Aug 8, 2025
f934d26
fix: remove tiptapblockimplementation
nperez0111 Aug 8, 2025
060108a
feat: Default blocks refactor fixes (#1953)
matthewlipski Aug 22, 2025
01478cd
Merge branch 'main' into default-blocks
nperez0111 Aug 25, 2025
cb8150c
refactor: settle the API a bit, migrate React to the new API, update …
nperez0111 Aug 26, 2025
ba57b93
feat: allow extending `blockSpecs` on a schema
nperez0111 Aug 28, 2025
71db2ba
build: trying to get the build to build again
nperez0111 Aug 28, 2025
36b9bd9
Merge branch 'main' into default-blocks
nperez0111 Aug 28, 2025
e41c137
chore: update prosemirror-model
nperez0111 Aug 28, 2025
1ddef1b
build: get things building again
nperez0111 Aug 28, 2025
62be3e2
test: get the tests further along
nperez0111 Aug 28, 2025
fda7be0
chore: rm unused
nperez0111 Aug 28, 2025
81c8d20
fix: better typing & support for hardBreakShortcut
nperez0111 Aug 28, 2025
ff2f039
Fixed export/copy test failures
matthewlipski Aug 28, 2025
fdbc419
Merge branch 'main' into default-blocks
nperez0111 Aug 29, 2025
41b2e43
fix: update the extend types
nperez0111 Aug 29, 2025
cf31ed6
chore: avoid withMultiColumn
nperez0111 Aug 29, 2025
a6ea62d
Fixed React export tests
matthewlipski Aug 29, 2025
703f86e
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into…
matthewlipski Aug 29, 2025
cd90895
Fixed most parse tests
matthewlipski Aug 29, 2025
258520f
Merge branch 'main' into default-blocks
matthewlipski Aug 31, 2025
dbc0c44
fix: minor typing bug
nperez0111 Sep 1, 2025
e3e4f5b
feat: inline content custom parse function
nperez0111 Sep 1, 2025
fc98fb8
Merge branch 'main' into default-blocks
nperez0111 Sep 1, 2025
ac0d392
Fixed remaining parse/paste issues
matthewlipski Sep 1, 2025
467b33d
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into…
matthewlipski Sep 1, 2025
f6cb1f9
Reverted example changes
matthewlipski Sep 1, 2025
a0e9443
Made select not render in code block if it has no options
matthewlipski Sep 1, 2025
5e3cad6
Updated unit test snaps
matthewlipski Sep 1, 2025
069f88f
chore: simpler `as const`
nperez0111 Sep 1, 2025
f4a0316
fix: rm heading & code-block opts
nperez0111 Sep 1, 2025
26b9b1c
Removed `codeBlock` and `heading` editor options & updated custom cod…
matthewlipski Sep 1, 2025
356aedc
Merge branch 'default-blocks-deprecate-options' into default-blocks
matthewlipski Sep 1, 2025
80963f4
Update examples
matthewlipski Sep 1, 2025
8ef6188
Updated code block docs & package export name
matthewlipski Sep 1, 2025
6b7bad8
chore: update comments
nperez0111 Sep 1, 2025
5dc2544
Fixed React custom block interactivity not working & added tests
matthewlipski Sep 1, 2025
87e57e3
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into…
matthewlipski Sep 1, 2025
17398cc
fix: update the types to have better inference based on options provided
nperez0111 Sep 2, 2025
95c493a
build: get build working again with better types
nperez0111 Sep 2, 2025
41d1449
build: finally get all the builds building
nperez0111 Sep 2, 2025
a92d1e7
fix: set default value
nperez0111 Sep 2, 2025
422cccb
fix: remove pageBreak from default schema
nperez0111 Sep 2, 2025
52a018c
refactor: move `meta` to block implementation instead of block config
nperez0111 Sep 2, 2025
84d50ce
test: update snap since order changed
nperez0111 Sep 2, 2025
05fbc87
chore: rm unused line
nperez0111 Sep 2, 2025
f242a2f
Minor fixes
matthewlipski Sep 2, 2025
59a7a63
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into…
matthewlipski Sep 2, 2025
b16a9ac
refactor: remove the strongly typed tiptap node abstraction
nperez0111 Sep 3, 2025
11f8633
feat: add `toExternalHTML` for custom inline content
nperez0111 Sep 3, 2025
b17a8d8
feat: support for react custom inline content
nperez0111 Sep 3, 2025
caf7ffd
Updated/fixed most e2e tests
matthewlipski Sep 3, 2025
b582be8
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into…
matthewlipski Sep 3, 2025
f9bca88
Updated remaining e2e snaps
matthewlipski Sep 3, 2025
ce882f1
fix: get the project to build again
nperez0111 Sep 3, 2025
eb049e2
refactor: remove `data-index` on numbered list item serialization
nperez0111 Sep 3, 2025
474897b
feat: support internal HTML as different from external HTML for inlin…
nperez0111 Sep 3, 2025
b4b56a9
chore: must please the linter
nperez0111 Sep 3, 2025
4b9d5d0
chore: minor changes
nperez0111 Sep 5, 2025
3f83c75
Merge branch 'main' into default-blocks
nperez0111 Sep 5, 2025
89ad4d5
Fixed old bug with HTML export not filling in unspecified props which…
matthewlipski Sep 5, 2025
6c0211a
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into…
matthewlipski Sep 5, 2025
52c7075
build: get build working again
nperez0111 Sep 9, 2025
4b40c57
refactor: use higher-level APIs
nperez0111 Sep 9, 2025
fdcfffb
refactor: export block types (and move blocks to separate sub-export)
nperez0111 Sep 9, 2025
3174b5b
chore: use helper function
nperez0111 Sep 9, 2025
cd788ad
refactor: move draggable to implementation
nperez0111 Sep 9, 2025
ddd3adf
fix: External HTML parse/export cases (#1991)
matthewlipski Sep 12, 2025
6e4beff
Merge branch 'main' into default-blocks
nperez0111 Sep 15, 2025
0246290
chore: rm console
nperez0111 Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/01-basic/01-minimal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@blocknote/ariakit": "latest",
"@blocknote/mantine": "latest",
"@blocknote/shadcn": "latest",
"@blocknote/code-block": "latest",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
Expand All @@ -24,4 +25,4 @@
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.3.4"
}
}
}
10 changes: 9 additions & 1 deletion examples/01-basic/01-minimal/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteSchema2 } from "@blocknote/core";
import { codeBlock } from "@blocknote/code-block";

const schema = BlockNoteSchema2.create(undefined, {
codeBlock,
});

export default function App() {
// Creates a new editor instance.
const editor = useCreateBlockNote();
const editor = useCreateBlockNote({
schema: schema as any,
});

// Renders the editor instance using a React component.
return <BlockNoteView editor={editor} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { EditorState } from "prosemirror-state";
import { EditorState, Transaction } from "prosemirror-state";

import {
getBlockInfo,
getNearestBlockPos,
} from "../../../getBlockInfoFromPos.js";
import { getPmSchema } from "../../../pmUtil.js";

export const splitBlockCommand = (
posInBlock: number,
Expand All @@ -17,33 +18,41 @@ export const splitBlockCommand = (
state: EditorState;
dispatch: ((args?: any) => any) | undefined;
}) => {
const nearestBlockContainerPos = getNearestBlockPos(state.doc, posInBlock);

const info = getBlockInfo(nearestBlockContainerPos);

if (!info.isBlockContainer) {
throw new Error(
`BlockContainer expected when calling splitBlock, position ${posInBlock}`,
);
}

const types = [
{
type: info.bnBlock.node.type, // always keep blockcontainer type
attrs: keepProps ? { ...info.bnBlock.node.attrs, id: undefined } : {},
},
{
type: keepType
? info.blockContent.node.type
: state.schema.nodes["paragraph"],
attrs: keepProps ? { ...info.blockContent.node.attrs } : {},
},
];

if (dispatch) {
state.tr.split(posInBlock, 2, types);
return splitBlockTr(state.tr, posInBlock, keepType, keepProps);
}

return true;
};
};

export const splitBlockTr = (
tr: Transaction,
posInBlock: number,
keepType?: boolean,
keepProps?: boolean,
): boolean => {
const nearestBlockContainerPos = getNearestBlockPos(tr.doc, posInBlock);

const info = getBlockInfo(nearestBlockContainerPos);

if (!info.isBlockContainer) {
return false;
}
const schema = getPmSchema(tr);

const types = [
{
type: info.bnBlock.node.type, // always keep blockcontainer type
attrs: keepProps ? { ...info.bnBlock.node.attrs, id: undefined } : {},
},
{
type: keepType ? info.blockContent.node.type : schema.nodes["paragraph"],
attrs: keepProps ? { ...info.blockContent.node.attrs } : {},
},
];

tr.split(posInBlock, 2, types);

return true;
};
130 changes: 130 additions & 0 deletions packages/core/src/blks/Audio/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { parseAudioElement } from "../../blocks/AudioBlockContent/parseAudioElement.js";
import { defaultProps } from "../../blocks/defaultProps.js";
import { parseFigureElement } from "../../blocks/FileBlockContent/helpers/parse/parseFigureElement.js";
import { createFileBlockWrapper } from "../../blocks/FileBlockContent/helpers/render/createFileBlockWrapper.js";
import { createFigureWithCaption } from "../../blocks/FileBlockContent/helpers/toExternalHTML/createFigureWithCaption.js";
import { createLinkWithCaption } from "../../blocks/FileBlockContent/helpers/toExternalHTML/createLinkWithCaption.js";
import {
createBlockConfig,
createBlockSpec,
} from "../../schema/blocks/playground.js";

export const FILE_AUDIO_ICON_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg>';

export interface AudioOptions {
icon?: string;
}
const config = createBlockConfig((_ctx: AudioOptions) => ({
type: "audio" as const,
propSchema: {
backgroundColor: defaultProps.backgroundColor,
// File name.
name: {
default: "" as const,
},
// File url.
url: {
default: "" as const,
},
// File caption.
caption: {
default: "" as const,
},

showPreview: {
default: true,
},
},
content: "none" as const,
meta: {
fileBlockAccept: ["audio/*"],
},
}));

export const definition = createBlockSpec(config).implementation((config) => ({
parse: (element) => {
if (element.tagName === "AUDIO") {
// Ignore if parent figure has already been parsed.
if (element.closest("figure")) {
return undefined;
}

return parseAudioElement(element as HTMLAudioElement);
}

if (element.tagName === "FIGURE") {
const parsedFigure = parseFigureElement(element, "audio");
if (!parsedFigure) {
return undefined;
}

const { targetElement, caption } = parsedFigure;

return {
...parseAudioElement(targetElement as HTMLAudioElement),
caption,
};
}

return undefined;
},
render: (block, editor) => {
const icon = document.createElement("div");
icon.innerHTML = config.icon ?? FILE_AUDIO_ICON_SVG;

const audio = document.createElement("audio");
audio.className = "bn-audio";
if (editor.resolveFileUrl) {
editor.resolveFileUrl(block.props.url).then((downloadUrl) => {
audio.src = downloadUrl;
});
} else {
audio.src = block.props.url;
}
audio.controls = true;
audio.contentEditable = "false";
audio.draggable = false;

return createFileBlockWrapper(
block,
editor,
{ dom: audio },
editor.dictionary.file_blocks.audio.add_button_text,
icon.firstElementChild as HTMLElement,
);
},
toExternalHTML(block) {
if (!block.props.url) {
const div = document.createElement("p");
div.textContent = "Add audio";

return {
dom: div,
};
}

let audio;
if (block.props.showPreview) {
audio = document.createElement("audio");
audio.src = block.props.url;
} else {
audio = document.createElement("a");
audio.href = block.props.url;
audio.textContent = block.props.name || block.props.url;
}

if (block.props.caption) {
if (block.props.showPreview) {
return createFigureWithCaption(audio, block.props.caption);
} else {
return createLinkWithCaption(audio, block.props.caption);
}
}

return {
dom: audio,
};
},
runsBefore: ["file"],
}));
100 changes: 100 additions & 0 deletions packages/core/src/blks/BulletListItem/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js";
import { defaultProps } from "../../blocks/defaultProps.js";
import { getListItemContent } from "../../blocks/ListItemBlockContent/getListItemContent.js";
import {
createBlockConfig,
createBlockNoteExtension,
createBlockSpec,
} from "../../schema/blocks/playground.js";
import { handleEnter } from "../utils/listItemEnterHandler.js";

const config = createBlockConfig(() => ({
type: "bulletListItem" as const,
propSchema: {
...defaultProps,
},
content: "inline",
}));

export const definition = createBlockSpec(config).implementation(
() => ({
parse(element) {
if (element.tagName !== "LI") {
return false;
}

const parent = element.parentElement;

if (parent === null) {
return false;
}

if (
parent.tagName === "UL" ||
(parent.tagName === "DIV" && parent.parentElement?.tagName === "UL")
) {
return {};
}

return false;
},
// As `li` elements can contain multiple paragraphs, we need to merge their contents
// into a single one so that ProseMirror can parse everything correctly.
parseContent: ({ el, schema }) =>
getListItemContent(el, schema, "bulletListItem"),
render() {
const div = document.createElement("div");
// We use a <p> tag, because for <li> tags we'd need a <ul> element to put
// them in to be semantically correct, which we can't have due to the
// schema.
const el = document.createElement("p");

div.appendChild(el);

return {
dom: div,
contentDOM: el,
};
},
}),
() => [
createBlockNoteExtension({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reasoning again to define keyboardshortcuts / inputrules in a separate argument + createBlockNoteExtension?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces keyboard shortcuts & inputrules to the extension API because we didn't have a way of doing them before.

I considered exposing them to both APIs (e.g. a custom block has an input rule API & so does the extension API), but that only added complexity.

Allowing extensions to be included to the block API is useful in that some things may require an extension to function (e.g. code block syntax highlighting, numbered list decorations).

key: "bullet-list-item-shortcuts",
keyboardShortcuts: {
Enter: ({ editor }) => {
return handleEnter(editor, "bulletListItem");
},
"Mod-Shift-8": ({ editor }) =>
editor.transact((tr) => {
const blockInfo = getBlockInfoFromTransaction(tr);

if (
!blockInfo.isBlockContainer ||
blockInfo.blockContent.node.type.spec.content !== "inline*"
) {
return true;
}

updateBlockTr(tr, blockInfo.bnBlock.beforePos, {
type: "bulletListItem",
props: {},
});
return true;
}),
},
inputRules: [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool that there's a BN-style API for this. Can we do the same for keyboardshortcuts you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about what that would look like for keyboard shortcuts, but the problem is that keyboard shortcuts can sort of just do anything. Some usecases I thought of were:

  • bringing up some menu items
  • triggering some action (e.g. stop AI generation)
  • inserting a new element
  • modifying an existing block

These sorts of cases are too disparate to really "unify" into a sensible API. Fundamental tension between composability & centralization.

{
find: new RegExp(`^[-+*]\\s$`),
replace() {
return {
type: "bulletListItem",
props: {},
content: [],
};
},
},
],
}),
],
);
Loading
Loading