UNPKG

@blocknote/core

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

1,331 lines (1,206 loc) 43.2 kB
import { createDocument, EditorOptions, FocusPosition, getSchema, Editor as TiptapEditor, } from "@tiptap/core"; import { type Command, type Plugin, 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 type { CollaborationOptions } from "../extensions/Collaboration/Collaboration.js"; import { BlockChangeExtension } 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 { 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; /** * When enabled, allows for collaboration between multiple users. * See [Real-time Collaboration](https://www.blocknotejs.org/docs/advanced/real-time-collaboration) for more info. */ collaboration?: CollaborationOptions; /** * 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>; /** * A replacement indicator to use when dragging and dropping blocks. Uses the [ProseMirror drop cursor](https://github.com/ProseMirror/prosemirror-dropcursor), or a modified version when [Column Blocks](https://www.blocknotejs.org/docs/document-structure#column-blocks) are enabled. * @remarks `() => Plugin` */ dropCursor?: (opts: { editor: BlockNoteEditor< NoInfer<BSchema>, NoInfer<ISchema>, NoInfer<SSchema> >; color?: string | false; width?: number; class?: string; }) => Plugin; /** * 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> >[]; /** * @deprecated, provide placeholders via dictionary instead * @internal */ placeholders?: Record< string | "default" | "emptyDocument", 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; }; /** * An option which user can pass with `false` value to disable the automatic creation of a trailing new block on the next line when the user types or edits any block. * * @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, }, }; // @ts-ignore 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 collaborationEnabled = this._extensionManager.hasExtension("ySync") || this._extensionManager.hasExtension("liveblocksExtension"); if (collaborationEnabled && newOptions.initialContent) { // eslint-disable-next-line no-console console.warn( "When using Collaboration, initialContent might cause conflicts, because changes should come from the collaboration provider", ); } const tiptapOptions: EditorOptions = { ...blockNoteTipTapOptions, ...newOptions._tiptapOptions, element: null, autofocus: newOptions.autofocus ?? false, extensions: tiptapExtensions, editorProps: { ...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", ...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 || (collaborationEnabled ? [ { type: "paragraph", id: "initialBlockId", }, ] : [ { 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: " + initialContent, ); } const schema = getSchema(tiptapOptions.extensions!); 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 }, ); } // When y-prosemirror creates an empty document, the `blockContainer` node is created with an `id` of `null`. // This causes the unique id extension to generate a new id for the initial block, which is not what we want // Since it will be randomly generated & cause there to be more updates to the ydoc // This is a hack to make it so that anytime `schema.doc.createAndFill` is called, the initial block id is already set to "initialBlockId" let cache: Node | undefined = undefined; const oldCreateAndFill = this.pmSchema.nodes.doc.createAndFill; this.pmSchema.nodes.doc.createAndFill = (...args: any) => { if (cache) { return cache; } const ret = oldCreateAndFill.apply(this.pmSchema.nodes.doc, args)!; // create a copy that we can mutate (otherwise, assigning attrs is not safe and corrupts the pm state) const jsonNode = JSON.parse(JSON.stringify(ret.toJSON())); jsonNode.content[0].content[0].attrs.id = "initialBlockId"; cache = Node.fromJSON(this.pmSchema, jsonNode); return cache; }; this.pmSchema.cached.blockNoteEditor = this; // 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; /** * Get an extension from the editor */ public getExtension: ExtensionManager["getExtension"] = (( ...args: Parameters<ExtensionManager["getExtension"]> ) => this._extensionManager.getExtension(...args)) as any; /** * Mount the editor to a DOM element. * * @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting */ public mount = (element: HTMLElement) => { this._tiptapEditor.mount({ mount: element }); }; /** * Unmount the editor from the DOM element it is bound to */ public unmount = () => { 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; } public isFocused() { if (this.headless) { return false; } return this.prosemirrorView?.hasFocus() || false; } public get headless() { return !this._tiptapEditor.isInitialized; } /** * 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); } /** * 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. */ public moveBlocksUp() { return this._blockManager.moveBlocksUp(); } /** * 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. */ public moveBlocksDown() { return this._blockManager.moveBlocksDown(); } /** * 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, ) { 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, ) { 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); } }