@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
1,445 lines (1,316 loc) • 47.2 kB
text/typescript
import {
createDocument,
EditorOptions,
FocusPosition,
getSchema,
Editor as TiptapEditor,
} from "@tiptap/core";
import { type Command, type Transaction } from "@tiptap/pm/state";
import { Node, Schema } from "prosemirror-model";
import type { BlocksChanged } from "../api/getBlocksChangedByTransaction.js";
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
import {
Block,
BlockNoteSchema,
DefaultBlockSchema,
DefaultInlineContentSchema,
DefaultStyleSchema,
PartialBlock,
} from "../blocks/index.js";
import {
BlockChangeExtension,
DropCursorOptions,
} from "../extensions/index.js";
import { UniqueID } from "../extensions/tiptap-extensions/UniqueID/UniqueID.js";
import type { Dictionary } from "../i18n/dictionary.js";
import { en } from "../i18n/locales/index.js";
import type {
BlockIdentifier,
BlockNoteDOMAttributes,
BlockSchema,
BlockSpecs,
CustomBlockNoteSchema,
InlineContentSchema,
InlineContentSpecs,
PartialInlineContent,
Styles,
StyleSchema,
StyleSpecs,
} from "../schema/index.js";
import "../style.css";
import { mergeCSSClasses } from "../util/browser.js";
import { EventEmitter } from "../util/EventEmitter.js";
import type { NoInfer } from "../util/typescript.js";
import {
Extension,
ExtensionFactory,
ExtensionFactoryInstance,
} from "./BlockNoteExtension.js";
import type { TextCursorPosition } from "./cursorPositionTypes.js";
import {
BlockManager,
EventManager,
ExportManager,
ExtensionManager,
SelectionManager,
StateManager,
StyleManager,
} from "./managers/index.js";
import type { Selection } from "./selectionTypes.js";
import { transformPasted } from "./transformPasted.js";
export type BlockCache<
BSchema extends BlockSchema = any,
ISchema extends InlineContentSchema = any,
SSchema extends StyleSchema = any,
> = WeakMap<Node, Block<BSchema, ISchema, SSchema>>;
export interface BlockNoteEditorOptions<
BSchema extends BlockSchema,
ISchema extends InlineContentSchema,
SSchema extends StyleSchema,
> {
/**
* Whether changes to blocks (like indentation, creating lists, changing headings) should be animated or not. Defaults to `true`.
*
* @default true
*/
animations?: boolean;
/**
* Whether the editor should be focused automatically when it's created.
*
* @default false
*/
autofocus?: FocusPosition;
/**
* Use default BlockNote font and reset the styles of <p> <li> <h1> elements etc., that are used in BlockNote.
*
* @default true
*/
defaultStyles?: boolean;
/**
* A dictionary object containing translations for the editor.
*
* See [Localization / i18n](https://www.blocknotejs.org/docs/advanced/localization) for more info.
*
* @remarks `Dictionary` is a type that contains all the translations for the editor.
*/
dictionary?: Dictionary & Record<string, any>;
/**
* Disable internal extensions (based on keys / extension name)
*
* @note Advanced
*/
disableExtensions?: string[];
/**
* An object containing attributes that should be added to HTML elements of the editor.
*
* See [Adding DOM Attributes](https://www.blocknotejs.org/docs/theming#adding-dom-attributes) for more info.
*
* @example { editor: { class: "my-editor-class" } }
* @remarks `Record<string, Record<string, string>>`
*/
domAttributes?: Partial<BlockNoteDOMAttributes>;
/**
* Options for configuring the drop cursor behavior when dragging and dropping blocks.
* Allows customization of cursor appearance and drop position computation through hooks.
* @remarks `DropCursorOptions`
*/
dropCursor?: DropCursorOptions;
/**
* The content that should be in the editor when it's created, represented as an array of {@link PartialBlock} objects.
*
* See [Partial Blocks](https://www.blocknotejs.org/docs/editor-api/manipulating-blocks#partial-blocks) for more info.
*
* @remarks `PartialBlock[]`
*/
initialContent?: PartialBlock<
NoInfer<BSchema>,
NoInfer<ISchema>,
NoInfer<SSchema>
>[];
/**
* Options for configuring how links behave in the editor.
*/
links?: {
/**
* HTML attributes to add to rendered link elements.
*
* @default {}
* @example { class: "my-link-class", target: "_blank" }
*/
HTMLAttributes?: Record<string, any>;
/**
* Custom handler invoked when a link is clicked. If left `undefined`,
* links are opened in a new window on click. If provided, the default
* open-on-click behavior is disabled and this function is called instead.
*
* Return `false` to let ProseMirror continue handling the click event.
* Returning `true` or nothing (the default) marks the event as handled.
*/
onClick?: (
event: MouseEvent,
editor: BlockNoteEditor<any, any, any>,
) => boolean | void;
/**
* Callback that decides whether a given `href` is a valid link. Applied at
* every gate where a link enters the document: HTML import, HTML export,
* paste, and autolink. Useful for supporting additional URI schemes (e.g.
* `vscode:`, `myapp:`) or tightening the default allowlist.
*
* Defaults to `isAllowedUri`, which allows
* `http|https|ftp|ftps|mailto|tel|callto|sms|cid|xmpp`. Import
* `isAllowedUri` from `@blocknote/core` to layer on top of the default.
*
* @example
* ```ts
* import { isAllowedUri } from "@blocknote/core";
*
* BlockNoteEditor.create({
* links: {
* isValidLink: (href) =>
* isAllowedUri(href) || href.startsWith("myapp:"),
* },
* });
* ```
*/
isValidLink?: (href: string) => boolean;
};
/**
* @deprecated, provide placeholders via dictionary instead
* @internal
*/
placeholders?: Record<string, string | undefined>;
/**
* Custom paste handler that can be used to override the default paste behavior.
*
* See [Paste Handling](https://www.blocknotejs.org/docs/advanced/paste-handling) for more info.
*
* @remarks `PasteHandler`
* @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
*
* @example
* ```ts
* pasteHandler: ({ defaultPasteHandler }) => {
* return defaultPasteHandler({ pasteBehavior: "prefer-html" });
* }
* ```
*/
pasteHandler?: (context: {
event: ClipboardEvent;
editor: BlockNoteEditor<
NoInfer<BSchema>,
NoInfer<ISchema>,
NoInfer<SSchema>
>;
/**
* The default paste handler
* @param context The context object
* @returns Whether the paste event was handled or not
*/
defaultPasteHandler: (context?: {
/**
* Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard.
* @default true
*/
prioritizeMarkdownOverHTML?: boolean;
/**
* Whether to parse `text/plain` content from the clipboard as Markdown content.
* @default true
*/
plainTextAsMarkdown?: boolean;
}) => boolean | undefined;
}) => boolean | undefined;
/**
* Resolve a URL of a file block to one that can be displayed or downloaded. This can be used for creating authenticated URL or
* implementing custom protocols / schemes
* @returns The URL that's
*/
resolveFileUrl?: (url: string) => Promise<string>;
/**
* The schema of the editor. The schema defines which Blocks, InlineContent, and Styles are available in the editor.
*
* See [Custom Schemas](https://www.blocknotejs.org/docs/custom-schemas) for more info.
* @remarks `BlockNoteSchema`
*/
schema: CustomBlockNoteSchema<BSchema, ISchema, SSchema>;
/**
* A flag indicating whether to set an HTML ID for every block
*
* When set to `true`, on each block an id attribute will be set with the block id
* Otherwise, the HTML ID attribute will not be set.
*
* (note that the id is always set on the `data-id` attribute)
*/
setIdAttribute?: boolean;
/**
* Determines behavior when pressing Tab (or Shift-Tab) while multiple blocks are selected and a toolbar is open.
* - `"prefer-navigate-ui"`: Changes focus to the toolbar. User must press Escape to close toolbar before indenting blocks. Better for keyboard accessibility.
* - `"prefer-indent"`: Always indents selected blocks, regardless of toolbar state. Keyboard navigation of toolbars not possible.
* @default "prefer-navigate-ui"
*/
tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
/**
* Allows enabling / disabling features of tables.
*
* See [Tables](https://www.blocknotejs.org/docs/editor-basics/document-structure#tables) for more info.
*
* @remarks `TableConfig`
*/
tables?: {
/**
* Whether to allow splitting and merging cells within a table.
*
* @default false
*/
splitCells?: boolean;
/**
* Whether to allow changing the background color of cells.
*
* @default false
*/
cellBackgroundColor?: boolean;
/**
* Whether to allow changing the text color of cells.
*
* @default false
*/
cellTextColor?: boolean;
/**
* Whether to allow changing cells into headers.
*
* @default false
*/
headers?: boolean;
};
/**
* When the editor document doesn't end in an empty paragraph block, this option causes the editor to render an element simulating one.
* When clicked by the user, it gets turned into an actual block at the end of the document. This element is not shown when the option is `false`.
*
* @default true
*/
trailingBlock?: boolean;
/**
* The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload).
* This method should set when creating the editor as this is application-specific.
*
* `undefined` means the application doesn't support file uploads.
*
* @param file The file that should be uploaded.
* @returns The URL of the uploaded file OR an object containing props that should be set on the file block (such as an id)
* @remarks `(file: File) => Promise<UploadFileResult>`
*/
uploadFile?: (
file: File,
blockId?: string,
) => Promise<string | Record<string, any>>;
/**
* additional tiptap options, undocumented
* @internal
*/
_tiptapOptions?: Partial<EditorOptions>;
/**
* Register extensions to the editor.
*
* See [Extensions](/docs/features/extensions) for more info.
*
* @remarks `ExtensionFactory[]`
*/
extensions?: Array<ExtensionFactoryInstance>;
}
const blockNoteTipTapOptions = {
enableInputRules: true,
enablePasteRules: true,
enableCoreExtensions: false,
};
export class BlockNoteEditor<
BSchema extends BlockSchema = DefaultBlockSchema,
ISchema extends InlineContentSchema = DefaultInlineContentSchema,
SSchema extends StyleSchema = DefaultStyleSchema,
> extends EventEmitter<{
create: void;
}> {
/**
* The underlying prosemirror schema
*/
public readonly pmSchema: Schema;
public readonly _tiptapEditor: TiptapEditor & {
contentComponent: any;
};
/**
* Used by React to store a reference to an `ElementRenderer` helper utility to make sure we can render React elements
* in the correct context (used by `ReactRenderUtil`)
*/
public elementRenderer: ((node: any, container: HTMLElement) => void) | null =
null;
/**
* Cache of all blocks. This makes sure we don't have to "recompute" blocks if underlying Prosemirror Nodes haven't changed.
* This is especially useful when we want to keep track of the same block across multiple operations,
* with this cache, blocks stay the same object reference (referential equality with ===).
*/
public blockCache: BlockCache = new WeakMap();
/**
* The dictionary contains translations for the editor.
*/
public readonly dictionary: Dictionary & Record<string, any>;
/**
* The schema of the editor. The schema defines which Blocks, InlineContent, and Styles are available in the editor.
*/
public readonly schema: BlockNoteSchema<BSchema, ISchema, SSchema>;
public readonly blockImplementations: BlockSpecs;
public readonly inlineContentImplementations: InlineContentSpecs;
public readonly styleImplementations: StyleSpecs;
/**
* The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload).
* This method should set when creating the editor as this is application-specific.
*
* `undefined` means the application doesn't support file uploads.
*
* @param file The file that should be uploaded.
* @returns The URL of the uploaded file OR an object containing props that should be set on the file block (such as an id)
*/
public readonly uploadFile:
| ((file: File, blockId?: string) => Promise<string | Record<string, any>>)
| undefined;
private onUploadStartCallbacks: ((blockId?: string) => void)[] = [];
private onUploadEndCallbacks: ((blockId?: string) => void)[] = [];
public readonly resolveFileUrl?: (url: string) => Promise<string>;
/**
* Editor settings
*/
public readonly settings: {
tables: {
splitCells: boolean;
cellBackgroundColor: boolean;
cellTextColor: boolean;
headers: boolean;
};
};
public static create<
Options extends Partial<BlockNoteEditorOptions<any, any, any>> | undefined,
>(
options?: Options,
): Options extends {
schema: CustomBlockNoteSchema<infer BSchema, infer ISchema, infer SSchema>;
}
? BlockNoteEditor<BSchema, ISchema, SSchema>
: BlockNoteEditor<
DefaultBlockSchema,
DefaultInlineContentSchema,
DefaultStyleSchema
> {
return new BlockNoteEditor(options ?? {}) as any;
}
protected constructor(
protected readonly options: Partial<
BlockNoteEditorOptions<BSchema, ISchema, SSchema>
>,
) {
super();
this.dictionary = options.dictionary || en;
this.settings = {
tables: {
splitCells: options?.tables?.splitCells ?? false,
cellBackgroundColor: options?.tables?.cellBackgroundColor ?? false,
cellTextColor: options?.tables?.cellTextColor ?? false,
headers: options?.tables?.headers ?? false,
},
};
// apply defaults
const newOptions = {
defaultStyles: true,
schema:
options.schema ||
(BlockNoteSchema.create() as unknown as CustomBlockNoteSchema<
BSchema,
ISchema,
SSchema
>),
...options,
placeholders: {
...this.dictionary.placeholders,
...options.placeholders,
},
};
this.schema = newOptions.schema;
this.blockImplementations = newOptions.schema.blockSpecs;
this.inlineContentImplementations = newOptions.schema.inlineContentSpecs;
this.styleImplementations = newOptions.schema.styleSpecs;
// TODO this should just be an extension
if (newOptions.uploadFile) {
const uploadFile = newOptions.uploadFile;
this.uploadFile = async (file, blockId) => {
this.onUploadStartCallbacks.forEach((callback) =>
callback.apply(this, [blockId]),
);
try {
return await uploadFile(file, blockId);
} finally {
this.onUploadEndCallbacks.forEach((callback) =>
callback.apply(this, [blockId]),
);
}
};
}
this.resolveFileUrl = newOptions.resolveFileUrl;
this._eventManager = new EventManager(this as any);
this._extensionManager = new ExtensionManager(this, newOptions);
const tiptapExtensions = this._extensionManager.getTiptapExtensions();
const tiptapOptions: EditorOptions = {
...blockNoteTipTapOptions,
...newOptions._tiptapOptions,
element: null,
autofocus: newOptions.autofocus ?? false,
extensions: tiptapExtensions,
editorProps: {
scrollMargin: { top: 72, bottom: 72, left: 0, right: 0 },
...newOptions._tiptapOptions?.editorProps,
attributes: {
// As of TipTap v2.5.0 the tabIndex is removed when the editor is not
// editable, so you can't focus it. We want to revert this as we have
// UI behaviour that relies on it.
tabIndex: "0",
// eslint-disable-next-line @typescript-eslint/no-misused-spread
...newOptions._tiptapOptions?.editorProps?.attributes,
...newOptions.domAttributes?.editor,
class: mergeCSSClasses(
"bn-editor",
newOptions.defaultStyles ? "bn-default-styles" : "",
newOptions.domAttributes?.editor?.class || "",
),
},
transformPasted,
},
} as any;
try {
const initialContent = newOptions.initialContent || [
{
type: "paragraph",
id: UniqueID.options.generateID(),
},
];
if (!Array.isArray(initialContent) || initialContent.length === 0) {
throw new Error(
"initialContent must be a non-empty array of blocks, received: " +
JSON.stringify(initialContent),
);
}
const schema = getSchema(tiptapOptions.extensions!);
// `blockToNode` (via `isPlainContentNodeType`) resolves the block schema
// through `schema.cached.blockNoteEditor`, so stamp it on this throwaway
// schema now — the real `pmSchema` is stamped separately below.
schema.cached.blockNoteEditor = this;
const pmNodes = initialContent.map((b) =>
blockToNode(b, schema, this.schema.styleSchema).toJSON(),
);
const doc = createDocument(
{
type: "doc",
content: [
{
type: "blockGroup",
content: pmNodes,
},
],
},
schema,
tiptapOptions.parseOptions,
);
this._tiptapEditor = new TiptapEditor({
...tiptapOptions,
content: doc.toJSON(),
}) as any;
this.pmSchema = this._tiptapEditor.schema;
} catch (e) {
throw new Error(
"Error creating document from blocks passed as `initialContent`",
{ cause: e },
);
}
this.pmSchema.cached.blockNoteEditor = this;
this._tiptapEditor.on("mount", () => {
this.headless = false;
});
this._tiptapEditor.on("unmount", () => {
this.headless = true;
});
// Initialize managers
this._blockManager = new BlockManager(this as any);
this._exportManager = new ExportManager(this as any);
this._selectionManager = new SelectionManager(this as any);
this._stateManager = new StateManager(this as any);
this._styleManager = new StyleManager(this as any);
this.emit("create");
}
// Manager instances
private readonly _blockManager: BlockManager<any, any, any>;
private readonly _eventManager: EventManager<any, any, any>;
private readonly _exportManager: ExportManager<any, any, any>;
private readonly _extensionManager: ExtensionManager;
private readonly _selectionManager: SelectionManager<any, any, any>;
private readonly _stateManager: StateManager;
private readonly _styleManager: StyleManager<any, any, any>;
/**
* BlockNote extensions that are added to the editor, keyed by the extension key
*/
public get extensions() {
return this._extensionManager.getExtensions();
}
/**
* Execute a prosemirror command. This is mostly for backwards compatibility with older code.
*
* @note You should prefer the {@link transact} method when possible, as it will automatically handle the dispatching of the transaction and work across blocknote transactions.
*
* @example
* ```ts
* editor.exec((state, dispatch, view) => {
* dispatch(state.tr.insertText("Hello, world!"));
* });
* ```
*/
public exec(command: Command) {
return this._stateManager.exec(command);
}
/**
* Check if a command can be executed. A command should return `false` if it is not valid in the current state.
*
* @example
* ```ts
* if (editor.canExec(command)) {
* // show button
* } else {
* // hide button
* }
* ```
*/
public canExec(command: Command): boolean {
return this._stateManager.canExec(command);
}
/**
* Execute a function within a "blocknote transaction".
* All changes to the editor within the transaction will be grouped together, so that
* we can dispatch them as a single operation (thus creating only a single undo step)
*
* @note There is no need to dispatch the transaction, as it will be automatically dispatched when the callback is complete.
*
* @example
* ```ts
* // All changes to the editor will be grouped together
* editor.transact((tr) => {
* tr.insertText("Hello, world!");
* // These two operations will be grouped together in a single undo step
* editor.transact((tr) => {
* tr.insertText("Hello, world!");
* });
* });
* ```
*/
public transact<T>(
callback: (
/**
* The current active transaction, this will automatically be dispatched to the editor when the callback is complete
* If another `transact` call is made within the callback, it will be passed the same transaction as the parent call.
*/
tr: Transaction,
) => T,
): T {
return this._stateManager.transact(callback);
}
/**
* Remove extension(s) from the editor
*/
public unregisterExtension: ExtensionManager["unregisterExtension"] = (
...args: Parameters<ExtensionManager["unregisterExtension"]>
) => this._extensionManager.unregisterExtension(...args);
/**
* Register extension(s) to the editor
*/
public registerExtension: ExtensionManager["registerExtension"] = (
...args: Parameters<ExtensionManager["registerExtension"]>
) => this._extensionManager.registerExtension(...args) as any;
/**
* Atomically unregister old extensions and register new ones in a single
* plugin update, avoiding re-entrant dispatch issues.
*/
public replaceExtension: ExtensionManager["replaceExtension"] = (
...args: Parameters<ExtensionManager["replaceExtension"]>
) => this._extensionManager.replaceExtension(...args);
/**
* Get an extension from the editor
*/
// Declared as an explicit intersection of the two `ExtensionManager`
// overloads rather than `ExtensionManager["getExtension"]`: indexed access on
// an overloaded method collapses the signatures, which widened the factory
// overload's `ReturnType<ReturnType<T>>` result to `any` (losing e.g. a
// returned extension's `store` type).
public getExtension: (<
const Ext extends Extension | ExtensionFactory = Extension,
>(
extension: string,
) =>
| (Ext extends Extension
? Ext
: Ext extends ExtensionFactory
? ReturnType<ReturnType<Ext>>
: never)
| undefined) &
(<const T extends ExtensionFactory>(
extension: T,
) => ReturnType<ReturnType<T>> | undefined) = ((extension: any) =>
this._extensionManager.getExtension(extension)) as any;
/**
* Mount the editor to a DOM element.
*
* @param element The DOM element to mount the editor's contenteditable into.
* @param options.portalTarget Where to mount `editor.portalElement` — the
* container that floating UI (toolbars, menus, etc) portals into. When
* omitted, defaults to `element.parentElement` (which is the editor's
* `bn-container` in typical React usage), or to `document.body` /
* the surrounding shadow root when no parent is available.
*
* @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
*/
public mount = (
element: HTMLElement,
options?: { portalTarget?: HTMLElement | null },
) => {
const root = element.getRootNode();
const isInShadowRoot =
typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot;
const target =
options?.portalTarget ??
element.parentElement ??
(isInShadowRoot ? (root as ShadowRoot) : document.body);
target.appendChild(this.portalElement);
this._tiptapEditor.mount({ mount: element });
};
/**
* Unmount the editor from the DOM element it is bound to
*/
public unmount = () => {
this.portalElement?.remove();
this._tiptapEditor.unmount();
};
/**
* Get the underlying prosemirror state
* @note Prefer using `editor.transact` to read the current editor state, as that will ensure the state is up to date
* @see https://prosemirror.net/docs/ref/#state.EditorState
*/
public get prosemirrorState() {
return this._stateManager.prosemirrorState;
}
/**
* Get the underlying prosemirror view
* @see https://prosemirror.net/docs/ref/#view.EditorView
*/
public get prosemirrorView() {
return this._stateManager.prosemirrorView;
}
public get domElement() {
if (this.headless) {
return undefined;
}
return this.prosemirrorView?.dom as HTMLDivElement | undefined;
}
private _portalElement: HTMLElement | undefined;
/**
* The portal container element at `document.body` used by floating UI
* elements (menus, toolbars) to escape overflow:hidden ancestors.
* Set by BlockNoteView; undefined in headless mode.
*/
public get portalElement() {
if (typeof document === "undefined") {
throw new Error(
"Portal element accessed, but not available in headless mode",
);
}
if (!this._portalElement) {
this._portalElement = document.createElement("div");
}
return this._portalElement;
}
/**
* Checks whether a DOM element belongs to this editor — either inside the
* editor's DOM tree or inside its portal container (used for floating UI
* elements like menus and toolbars).
*/
public isWithinEditor = (element: Element): boolean => {
return !!(
this.domElement?.parentElement?.contains(element) ||
this.portalElement?.contains(element)
);
};
public isFocused() {
if (this.headless) {
return false;
}
return this.prosemirrorView?.hasFocus() || false;
}
public headless = true;
/**
* Focus on the editor
*/
public focus() {
if (this.headless) {
return;
}
this.prosemirrorView.focus();
}
/**
* Blur the editor
*/
public blur() {
if (this.headless) {
return;
}
this.domElement?.blur();
}
// TODO move to extension
public onUploadStart(callback: (blockId?: string) => void) {
this.onUploadStartCallbacks.push(callback);
return () => {
const index = this.onUploadStartCallbacks.indexOf(callback);
if (index > -1) {
this.onUploadStartCallbacks.splice(index, 1);
}
};
}
public onUploadEnd(callback: (blockId?: string) => void) {
this.onUploadEndCallbacks.push(callback);
return () => {
const index = this.onUploadEndCallbacks.indexOf(callback);
if (index > -1) {
this.onUploadEndCallbacks.splice(index, 1);
}
};
}
/**
* @deprecated, use `editor.document` instead
*/
public get topLevelBlocks(): Block<BSchema, ISchema, SSchema>[] {
return this.document;
}
/**
* Gets a snapshot of all top-level (non-nested) blocks in the editor.
* @returns A snapshot of all top-level (non-nested) blocks in the editor.
*/
public get document(): Block<BSchema, ISchema, SSchema>[] {
return this._blockManager.document;
}
/**
* Gets a snapshot of an existing block from the editor.
* @param blockIdentifier The identifier of an existing block that should be
* retrieved.
* @returns The block that matches the identifier, or `undefined` if no
* matching block was found.
*/
public getBlock(
blockIdentifier: BlockIdentifier,
): Block<BSchema, ISchema, SSchema> | undefined {
return this._blockManager.getBlock(blockIdentifier);
}
/**
* Gets a snapshot of the previous sibling of an existing block from the
* editor.
* @param blockIdentifier The identifier of an existing block for which the
* previous sibling should be retrieved.
* @returns The previous sibling of the block that matches the identifier.
* `undefined` if no matching block was found, or it's the first child/block
* in the document.
*/
public getPrevBlock(
blockIdentifier: BlockIdentifier,
): Block<BSchema, ISchema, SSchema> | undefined {
return this._blockManager.getPrevBlock(blockIdentifier);
}
/**
* Gets a snapshot of the next sibling of an existing block from the editor.
* @param blockIdentifier The identifier of an existing block for which the
* next sibling should be retrieved.
* @returns The next sibling of the block that matches the identifier.
* `undefined` if no matching block was found, or it's the last child/block in
* the document.
*/
public getNextBlock(
blockIdentifier: BlockIdentifier,
): Block<BSchema, ISchema, SSchema> | undefined {
return this._blockManager.getNextBlock(blockIdentifier);
}
/**
* Gets a snapshot of the parent of an existing block from the editor.
* @param blockIdentifier The identifier of an existing block for which the
* parent should be retrieved.
* @returns The parent of the block that matches the identifier. `undefined`
* if no matching block was found, or the block isn't nested.
*/
public getParentBlock(
blockIdentifier: BlockIdentifier,
): Block<BSchema, ISchema, SSchema> | undefined {
return this._blockManager.getParentBlock(blockIdentifier);
}
/**
* Traverses all blocks in the editor depth-first, and executes a callback for each.
* @param callback The callback to execute for each block. Returning `false` stops the traversal.
* @param reverse Whether the blocks should be traversed in reverse order.
*/
public forEachBlock(
callback: (block: Block<BSchema, ISchema, SSchema>) => boolean,
reverse = false,
): void {
this._blockManager.forEachBlock(callback, reverse);
}
/**
* Executes a callback whenever the editor's contents change.
* @param callback The callback to execute.
*
* @deprecated use {@link BlockNoteEditor.onChange} instead
*/
public onEditorContentChange(callback: () => void) {
this._tiptapEditor.on("update", callback);
}
/**
* Executes a callback whenever the editor's selection changes.
* @param callback The callback to execute.
*
* @deprecated use `onSelectionChange` instead
*/
public onEditorSelectionChange(callback: () => void) {
this._tiptapEditor.on("selectionUpdate", callback);
}
/**
* Executes a callback before any change is applied to the editor, allowing you to cancel the change.
* @param callback The callback to execute.
* @returns A function to remove the callback.
*/
public onBeforeChange(
callback: (context: {
getChanges: () => BlocksChanged<BSchema, ISchema, SSchema>;
tr: Transaction;
}) => boolean | void,
): () => void {
return this._extensionManager
.getExtension(BlockChangeExtension)!
.subscribe(callback);
}
/**
* Gets a snapshot of the current text cursor position.
* @returns A snapshot of the current text cursor position.
*/
public getTextCursorPosition(): TextCursorPosition<
BSchema,
ISchema,
SSchema
> {
return this._selectionManager.getTextCursorPosition();
}
/**
* Sets the text cursor position to the start or end of an existing block. Throws an error if the target block could
* not be found.
* @param targetBlock The identifier of an existing block that the text cursor should be moved to.
* @param placement Whether the text cursor should be placed at the start or end of the block.
*/
public setTextCursorPosition(
targetBlock: BlockIdentifier,
placement: "start" | "end" = "start",
) {
return this._selectionManager.setTextCursorPosition(targetBlock, placement);
}
/**
* Gets a snapshot of the current selection. This contains all blocks (included nested blocks)
* that the selection spans across.
*
* If the selection starts / ends halfway through a block, the returned data will contain the entire block.
*/
public getSelection(): Selection<BSchema, ISchema, SSchema> | undefined {
return this._selectionManager.getSelection();
}
/**
* Gets a snapshot of the current selection. This contains all blocks (included nested blocks)
* that the selection spans across.
*
* If the selection starts / ends halfway through a block, the returned block will be
* only the part of the block that is included in the selection.
*/
public getSelectionCutBlocks(expandToWords = false) {
return this._selectionManager.getSelectionCutBlocks(expandToWords);
}
/**
* Sets the selection to a range of blocks.
* @param startBlock The identifier of the block that should be the start of the selection.
* @param endBlock The identifier of the block that should be the end of the selection.
*/
public setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier) {
return this._selectionManager.setSelection(startBlock, endBlock);
}
/**
* Checks if the editor is currently editable, or if it's locked.
* @returns True if the editor is editable, false otherwise.
*/
public get isEditable(): boolean {
return this._stateManager.isEditable;
}
/**
* Makes the editor editable or locks it, depending on the argument passed.
* @param editable True to make the editor editable, or false to lock it.
*/
public set isEditable(editable: boolean) {
this._stateManager.isEditable = editable;
}
/**
* Inserts new blocks into the editor. If a block's `id` is undefined, BlockNote generates one automatically. Throws an
* error if the reference block could not be found.
* @param blocksToInsert An array of partial blocks that should be inserted.
* @param referenceBlock An identifier for an existing block, at which the new blocks should be inserted.
* @param placement Whether the blocks should be inserted just before, just after, or nested inside the
* `referenceBlock`.
*/
public insertBlocks(
blocksToInsert: PartialBlock<BSchema, ISchema, SSchema>[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" = "before",
) {
return this._blockManager.insertBlocks(
blocksToInsert,
referenceBlock,
placement,
);
}
/**
* Updates an existing block in the editor. Since updatedBlock is a PartialBlock object, some fields might not be
* defined. These undefined fields are kept as-is from the existing block. Throws an error if the block to update could
* not be found.
* @param blockToUpdate The block that should be updated.
* @param update A partial block which defines how the existing block should be changed.
*/
public updateBlock(
blockToUpdate: BlockIdentifier,
update: PartialBlock<BSchema, ISchema, SSchema>,
) {
return this._blockManager.updateBlock(blockToUpdate, update);
}
/**
* Removes existing blocks from the editor. Throws an error if any of the blocks could not be found.
* @param blocksToRemove An array of identifiers for existing blocks that should be removed.
*/
public removeBlocks(blocksToRemove: BlockIdentifier[]) {
return this._blockManager.removeBlocks(blocksToRemove);
}
/**
* Replaces existing blocks in the editor with new blocks. If the blocks that should be removed are not adjacent or
* are at different nesting levels, `blocksToInsert` will be inserted at the position of the first block in
* `blocksToRemove`. Throws an error if any of the blocks to remove could not be found.
* @param blocksToRemove An array of blocks that should be replaced.
* @param blocksToInsert An array of partial blocks to replace the old ones with.
*/
public replaceBlocks(
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock<BSchema, ISchema, SSchema>[],
) {
return this._blockManager.replaceBlocks(blocksToRemove, blocksToInsert);
}
/**
* Undo the last action.
*/
public undo(): boolean {
return this._stateManager.undo();
}
/**
* Redo the last action.
*/
public redo(): boolean {
return this._stateManager.redo();
}
/**
* Insert a piece of content at the current cursor position.
*
* @param content can be a string, or array of partial inline content elements
*/
public insertInlineContent(
content: PartialInlineContent<ISchema, SSchema>,
{ updateSelection = false }: { updateSelection?: boolean } = {},
) {
this._styleManager.insertInlineContent(content, { updateSelection });
}
/**
* Gets the active text styles at the text cursor position or at the end of the current selection if it's active.
*/
public getActiveStyles(): Styles<SSchema> {
return this._styleManager.getActiveStyles();
}
/**
* Adds styles to the currently selected content.
* @param styles The styles to add.
*/
public addStyles(styles: Styles<SSchema>) {
this._styleManager.addStyles(styles);
}
/**
* Removes styles from the currently selected content.
* @param styles The styles to remove.
*/
public removeStyles(styles: Styles<SSchema>) {
this._styleManager.removeStyles(styles);
}
/**
* Toggles styles on the currently selected content.
* @param styles The styles to toggle.
*/
public toggleStyles(styles: Styles<SSchema>) {
this._styleManager.toggleStyles(styles);
}
/**
* Gets the currently selected text.
*/
public getSelectedText() {
return this._styleManager.getSelectedText();
}
/**
* Gets the URL of the last link in the current selection, or `undefined` if there are no links in the selection.
*/
public getSelectedLinkUrl() {
return this._styleManager.getSelectedLinkUrl();
}
/**
* Creates a new link to replace the selected content.
* @param url The link URL.
* @param text The text to display the link with.
*/
public createLink(url: string, text?: string) {
this._styleManager.createLink(url, text);
}
/**
* Find the link mark and its range at the given position.
* Returns undefined if there is no link at that position.
*/
public getLinkMarkAtPos(pos: number) {
return this._styleManager.getLinkMarkAtPos(pos);
}
/**
* Updates the link at the given position with a new URL and text.
* @param url The new link URL.
* @param text The new text to display.
* @param position The position inside the link to edit. Defaults to the current selection anchor.
*/
public editLink(url: string, text: string, position?: number) {
this._styleManager.editLink(url, text, position);
}
/**
* Removes the link at the given position, keeping the text.
* @param position The position inside the link to remove. Defaults to the current selection anchor.
*/
public deleteLink(position?: number) {
this._styleManager.deleteLink(position);
}
/**
* Checks if the block containing the text cursor can be nested.
*/
public canNestBlock() {
return this._blockManager.canNestBlock();
}
/**
* Nests the block containing the text cursor into the block above it.
*/
public nestBlock() {
this._blockManager.nestBlock();
}
/**
* Checks if the block containing the text cursor is nested.
*/
public canUnnestBlock() {
return this._blockManager.canUnnestBlock();
}
/**
* Lifts the block containing the text cursor out of its parent.
*/
public unnestBlock() {
this._blockManager.unnestBlock();
}
/**
* Moves the selected blocks up. If the previous block has children, moves
* them to the end of its children. If there is no previous block, but the
* current blocks share a common parent, moves them out of & before it. If a
* `blockIdentifier` is provided, that block is moved instead of the
* selection, and the selection is left unchanged.
*/
public moveBlocksUp(blockIdentifier?: BlockIdentifier) {
return this._blockManager.moveBlocksUp(blockIdentifier);
}
/**
* Moves the selected blocks down. If the next block has children, moves
* them to the start of its children. If there is no next block, but the
* current blocks share a common parent, moves them out of & after it. If a
* `blockIdentifier` is provided, that block is moved instead of the
* selection, and the selection is left unchanged.
*/
public moveBlocksDown(blockIdentifier?: BlockIdentifier) {
return this._blockManager.moveBlocksDown(blockIdentifier);
}
/**
* Exports blocks into a simplified HTML string. To better conform to HTML standards, children of blocks which aren't list
* items are un-nested in the output HTML.
*
* @param blocks An array of blocks that should be serialized into HTML.
* @returns The blocks, serialized as an HTML string.
*/
public blocksToHTMLLossy(
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.document,
): string {
return this._exportManager.blocksToHTMLLossy(blocks);
}
/**
* Serializes blocks into an HTML string in the format that would normally be rendered by the editor.
*
* Use this method if you want to server-side render HTML (for example, a blog post that has been edited in BlockNote)
* and serve it to users without loading the editor on the client (i.e.: displaying the blog post)
*
* @param blocks An array of blocks that should be serialized into HTML.
* @returns The blocks, serialized as an HTML string.
*/
public blocksToFullHTML(
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.document,
): string {
return this._exportManager.blocksToFullHTML(blocks);
}
/**
* Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and
* `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote
* doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text.
* @param html The HTML string to parse blocks from.
* @returns The blocks parsed from the HTML string.
*/
public tryParseHTMLToBlocks(
html: string,
): Block<BSchema, ISchema, SSchema>[] {
return this._exportManager.tryParseHTMLToBlocks(html);
}
/**
* Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of
* BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed.
* @param blocks An array of blocks that should be serialized into Markdown.
* @returns The blocks, serialized as a Markdown string.
*/
public blocksToMarkdownLossy(
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.document,
): string {
return this._exportManager.blocksToMarkdownLossy(blocks);
}
/**
* Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on
* Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it
* as text.
* @param markdown The Markdown string to parse blocks from.
* @returns The blocks parsed from the Markdown string.
*/
public tryParseMarkdownToBlocks(
markdown: string,
): Block<BSchema, ISchema, SSchema>[] {
return this._exportManager.tryParseMarkdownToBlocks(markdown);
}
/**
* A callback function that runs whenever the editor's contents change.
*
* @param callback The callback to execute.
* @returns A function to remove the callback.
*/
public onChange(
callback: (
editor: BlockNoteEditor<BSchema, ISchema, SSchema>,
context: {
/**
* Returns the blocks that were inserted, updated, or deleted by the change that occurred.
*/
getChanges(): BlocksChanged<BSchema, ISchema, SSchema>;
},
) => void,
/**
* If true, the callback will be triggered when the changes are caused by a remote user
* @default true
*/
includeUpdatesFromRemote?: boolean,
) {
return this._eventManager.onChange(callback, includeUpdatesFromRemote);
}
/**
* A callback function that runs whenever the text cursor position or selection changes.
*
* @param callback The callback to execute.
* @returns A function to remove the callback.
*/
public onSelectionChange(
callback: (editor: BlockNoteEditor<BSchema, ISchema, SSchema>) => void,
includeSelectionChangedByRemote?: boolean,
) {
return this._eventManager.onSelectionChange(
callback,
includeSelectionChangedByRemote,
);
}
/**
* A callback function that runs when the editor has been mounted.
*
* This can be useful for plugins to initialize themselves after the editor has been mounted.
*
* @param callback The callback to execute.
* @returns A function to remove the callback.
*/
public onMount(
callback: (ctx: {
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
}) => void,
) {
return this._eventManager.onMount(callback);
}
/**
* A callback function that runs when the editor has been unmounted.
*
* This can be useful for plugins to clean up themselves after the editor has been unmounted.
*
* @param callback The callback to execute.
* @returns A function to remove the callback.
*/
public onUnmount(
callback: (ctx: {
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
}) => void,
) {
return this._eventManager.onUnmount(callback);
}
/**
* Gets the bounding box of the current selection.
* @returns The bounding box of the current selection.
*/
public getSelectionBoundingBox() {
return this._selectionManager.getSelectionBoundingBox();
}
public get isEmpty() {
const doc = this.document;
// Note: only works for paragraphs as default blocks (but for now this is default in blocknote)
// checking prosemirror directly might be faster
return (
doc.length === 0 ||
(doc.length === 1 &&
doc[0].type === "paragraph" &&
(doc[0].content as any).length === 0)
);
}
/**
* Paste HTML into the editor. Defaults to converting HTML to BlockNote HTML.
* @param html The HTML to paste.
* @param raw Whether to paste the HTML as is, or to convert it to BlockNote HTML.
*/
public pasteHTML(html: string, raw = false) {
this._exportManager.pasteHTML(html, raw);
}
/**
* Paste text into the editor. Defaults to interpreting text as markdown.
* @param text The text to paste.
*/
public pasteText(text: string) {
return this._exportManager.pasteText(text);
}
/**
* Paste markdown into the editor.
* @param markdown The markdown to paste.
*/
public pasteMarkdown(markdown: string) {
return this._exportManager.pasteMarkdown(markdown);
}
}