# Blocks API Source: https://docs.yoopta.dev/api-reference/blocks Block-level operations for Yoopta Editor # Blocks API The Blocks API provides block-level operations: creation, deletion, movement, transformation, and navigation. It works above the Elements API, operating on entire blocks rather than elements within blocks. ## Accessing the API Import the `Blocks` namespace from `@yoopta/editor`: ```typescript theme={null} import { Blocks } from '@yoopta/editor'; Blocks.insertBlock(editor, 'Paragraph', { at: 0 }); Blocks.deleteBlock(editor, { at: 3 }); Blocks.moveBlock(editor, 'block-id', 5); ``` Blocks API methods are not on the editor instance — use the `Blocks` namespace. ## Type Definitions ```typescript theme={null} type YooptaPathIndex = number; type YooptaBlockData = { id: string; type: string; value: SlateElement[]; meta: { order: number; depth: number; align?: 'left' | 'center' | 'right'; }; }; ``` *** ## Methods ### insertBlock Creates and inserts a new block of the given type. ```typescript theme={null} Blocks.insertBlock( editor: YooEditor, type: string, options?: InsertBlockOptions ): string ``` **Parameters:** * `editor` — Editor instance (required). * `type` — Block type, e.g. `'Paragraph'`, `'Heading'` (required). * `options.at` — Position to insert; default: `editor.path.current`. * `options.focus` — Whether to focus the block after insertion. * `options.blockData` — Custom block data (id, meta, value). * `options.elements` — Custom element structure via `editor.y()`. **Returns:** ID of the new block. ```typescript theme={null} const blockId = Blocks.insertBlock(editor, 'Paragraph', { at: editor.path.current, focus: true, }); ``` *** ### deleteBlock Removes a block and optionally focuses another. ```typescript theme={null} Blocks.deleteBlock(editor: YooEditor, options?: DeleteBlockOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the block to delete. * `options.blockId` — ID of the block to delete (overrides `at` if both set). * `options.focus` — Whether to focus a block after deletion; default: `true`. * `options.focusTarget` — Which block to focus: `'previous'` | `'next'` | `'none'`; default: `'previous'`. ```typescript theme={null} Blocks.deleteBlock(editor, { at: 3, focusTarget: 'next' }); ``` *** ### updateBlock Updates a block's metadata and/or content. ```typescript theme={null} Blocks.updateBlock( editor: YooEditor, blockId: string, newData: Omit, 'id' | 'type'> ): void ``` **Parameters:** * `editor` — Editor instance (required). * `blockId` — ID of the block to update (required). * `newData.meta` — Metadata to update: `order`, `depth`, `align`. * `newData.value` — New block content (array of Slate elements). ```typescript theme={null} Blocks.updateBlock(editor, 'block-123', { meta: { align: 'center' }, }); ``` *** ### getBlock Retrieves block data by ID or path. ```typescript theme={null} Blocks.getBlock(editor: YooEditor, options: GetBlockOptions): YooptaBlockData | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position index of the block. * `options.id` — ID of the block (overrides `at` if both set). One of `at` or `id` is required. **Returns:** Block data or `null`. ```typescript theme={null} const block = Blocks.getBlock(editor, { id: 'block-123' }); ``` *** ### moveBlock Moves a block to a new position. ```typescript theme={null} Blocks.moveBlock( editor: YooEditor, draggedBlockId: string, newPath: number ): void ``` **Parameters:** * `editor` — Editor instance (required). * `draggedBlockId` — ID of the block to move (required). * `newPath` — New position (order index) for the block (required). ```typescript theme={null} Blocks.moveBlock(editor, 'block-123', 5); ``` *** ### focusBlock Focuses a block and optionally sets cursor position. ```typescript theme={null} Blocks.focusBlock( editor: YooEditor, blockId: string, options?: FocusBlockOptions ): void ``` **Parameters:** * `editor` — Editor instance (required). * `blockId` — ID of the block to focus (required). * `options.waitExecution` — Delay focus (e.g. until DOM is ready). * `options.waitExecutionMs` — Delay in milliseconds. * `options.shouldUpdateBlockPath` — Update `editor.path.current`; default: `true`. * `options.focusAt` — Cursor position: `Point` or `Path`; default: first node. * `options.slate` — Slate editor instance to use (optional). ```typescript theme={null} Blocks.focusBlock(editor, 'block-123', { focusAt: { path: [0, 0], offset: 5 } }); ``` *** ### duplicateBlock Creates a copy of a block at a given position. ```typescript theme={null} Blocks.duplicateBlock( editor: YooEditor, options?: DuplicateBlockOptions ): string | undefined ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the block to duplicate; default: `editor.path.current`. * `options.blockId` — ID of the block to duplicate (overrides `at`). * `options.insertAt` — Position to insert the duplicate; default: after original. * `options.focus` — Focus the new block after creation; default: `true`. * `options.elements` — Custom element structure for the duplicate. **Returns:** New block ID or `undefined`. ```typescript theme={null} const newId = Blocks.duplicateBlock(editor, { at: 2, insertAt: 4 }); ``` *** ### splitBlock Splits a block at the selection (or given position). ```typescript theme={null} Blocks.splitBlock( editor: YooEditor, options?: SplitBlockOptions ): string | undefined ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the block to split; default: `editor.path.current`. * `options.blockId` — ID of the block to split. * `options.splitAt` — Location to split at; default: current selection. * `options.focus` — Focus after split; default: `true`. * `options.focusTarget` — Which block to focus: `'new'` | `'original'` | `'none'`; default: `'new'`. * `options.preserveContent` — Keep content in both blocks; default: `true`. **Returns:** New block ID or `undefined`. ```typescript theme={null} const newBlockId = Blocks.splitBlock(editor, { focusTarget: 'new' }); ``` *** ### mergeBlock Merges a block into another (default: into previous). ```typescript theme={null} Blocks.mergeBlock(editor: YooEditor, options?: MergeBlockOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the source block (the one merged into target). * `options.blockId` — ID of the source block. * `options.targetAt` — Position of the target block (receives content); default: previous block. * `options.targetBlockId` — ID of the target block. * `options.focus` — Focus the target block after merge; default: `true`. * `options.preserveContent` — Keep content from source block; default: `true`. ```typescript theme={null} Blocks.mergeBlock(editor, { at: 5, targetAt: 3 }); ``` *** ### toggleBlock Transforms block type (e.g. Paragraph → Heading) or inserts element in leaf. ```typescript theme={null} Blocks.toggleBlock( editor: YooEditor, type: string, options?: ToggleBlockOptions ): string ``` **Parameters:** * `editor` — Editor instance (required). * `type` — Target block/element type, e.g. `'Heading'`, `'Paragraph'` (required). * `options.at` — Position of the block; default: `editor.path.current`. * `options.scope` — `'auto'` (default) | `'block'` (transform block) | `'element'` (insert in leaf). * `options.preserveContent` — Transfer text to new block/element; default: `true`. * `options.focus` — Focus after toggle; default: `false`. * `options.elements` — Custom element structure via `editor.y()`. **Returns:** Toggled block ID. ```typescript theme={null} Blocks.toggleBlock(editor, 'Heading', { preserveContent: true }); ``` *** ### increaseBlockDepth Increases block depth (indent). ```typescript theme={null} Blocks.increaseBlockDepth( editor: YooEditor, options?: BlockDepthOptions ): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the block; default: `editor.path.current`. * `options.blockId` — ID of the block (overrides `at`). ```typescript theme={null} Blocks.increaseBlockDepth(editor, { at: editor.path.current }); ``` *** ### decreaseBlockDepth Decreases block depth (outdent). ```typescript theme={null} Blocks.decreaseBlockDepth( editor: YooEditor, options?: BlockDepthOptions ): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the block; default: `editor.path.current`. * `options.blockId` — ID of the block (overrides `at`). ```typescript theme={null} Blocks.decreaseBlockDepth(editor, { blockId: 'block-123' }); ``` *** ### getBlockSlate Returns the Slate editor instance for a block. ```typescript theme={null} Blocks.getBlockSlate( editor: YooEditor, options: GetBlockSlateOptions ): SlateEditor | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Position of the block. * `options.id` — ID of the block (overrides `at`). One of `at` or `id` is required. **Returns:** Slate editor or `null`. ```typescript theme={null} const slate = Blocks.getBlockSlate(editor, { id: 'block-123' }); if (slate) { const text = Editor.string(slate, []); } ``` *** ### buildBlockData Builds a `YooptaBlockData` structure (does not insert into editor). ```typescript theme={null} Blocks.buildBlockData(block?: BuildBlockDataOptions): YooptaBlockData ``` **Parameters:** * `block.id` — Block ID; if omitted, a unique ID is generated. * `block.type` — Block type (plugin name); default: `'Paragraph'`. * `block.value` — Block content (array of Slate elements). * `block.meta` — Metadata: `order`, `depth`, `align`. **Returns:** Complete `YooptaBlockData` object. ```typescript theme={null} const blockData = Blocks.buildBlockData({ type: 'Heading', meta: { order: 0, depth: 0, align: 'center' }, }); ``` # Editor API Source: https://docs.yoopta.dev/api-reference/editor createYooptaEditor and YooEditor instance API # Editor API The Editor API is the entry point for Yoopta: `createYooptaEditor` creates an editor instance (`YooEditor`) that holds content, block/element methods, events, parsers, and history. Use it together with the [Blocks](/api-reference/blocks), [Elements](/api-reference/elements), and [Marks](/api-reference/marks) APIs. ## createYooptaEditor Creates a Yoopta editor instance. ```typescript theme={null} createYooptaEditor(options: CreateYooptaEditorOptions): YooEditor ``` **Parameters:** * `options.plugins` — Array of block plugins (required). * `options.marks` — Optional array of mark definitions (e.g. from `createYooptaMark`). * `options.value` — Optional initial content (`YooptaContentValue`). If invalid or omitted, one empty paragraph block is used. * `options.id` — Optional editor ID; a unique ID is generated if omitted. * `options.readOnly` — Optional; default: `false`. **Returns:** `YooEditor` instance. ```typescript theme={null} import { createYooptaEditor } from '@yoopta/editor'; import { ParagraphPlugin } from '@yoopta/paragraph'; const editor = createYooptaEditor({ plugins: [ParagraphPlugin], marks: [], value: undefined, readOnly: false, }); ``` *** ## Type Definitions ```typescript theme={null} type YooptaContentValue = Record; type YooptaPath = { current: number | null; selected?: number[] | null; selection?: Selection | null; source?: 'selection-box' | 'native-selection' | 'mousemove' | 'keyboard' | 'copy-paste' | null; }; type CreateYooptaEditorOptions = { id?: string; plugins: readonly YooptaPlugin[]; marks?: YooptaMark[]; value?: YooptaContentValue; readOnly?: boolean; }; ``` *** ## YooEditor instance The object returned by `createYooptaEditor` exposes the following. Block and element methods are documented in [Blocks API](/api-reference/blocks) and [Elements API](/api-reference/elements). ### Content * **`editor.children`** — Current content: `YooptaContentValue` (record of block id → block data). * **`editor.isEmpty()`** — Returns whether the editor has no meaningful content. * **`editor.getEditorValue()`** — Returns `editor.children` (current content). * **`editor.setEditorValue(value)`** — Replaces editor content with `value`; if `value` is `null` or invalid, resets to one empty paragraph block. **Parameters for setEditorValue:** * `value` — `YooptaContentValue | null`; use `null` or invalid value to reset. *** ### Block methods (on editor) Available on the instance; see [Blocks API](/api-reference/blocks) for full signatures and options. * `insertBlock`, `deleteBlock`, `updateBlock`, `getBlock` * `moveBlock`, `focusBlock`, `duplicateBlock` * `toggleBlock`, `splitBlock`, `mergeBlock` * `increaseBlockDepth`, `decreaseBlockDepth` * `setPath(path)` — Sets `editor.path` (e.g. current block index). `path: YooptaPath`. *** ### Element methods (on editor) Available on the instance; see [Elements API](/api-reference/elements) for full signatures and options. * `insertElement`, `updateElement`, `deleteElement` * `getElement`, `getElements`, `getElementEntry` * `getElementPath`, `getParentElementPath`, `getElementRect` * `getElementChildren`, `getRootElement`, `isElementEmpty` *** ### Element builder: editor.y Build Slate elements for use in block/element APIs. ```typescript theme={null} editor.y(type: string, options?: ElementStructureOptions): SlateElement editor.y.text(text: string, marks?: TextNodeOptions): SlateElementTextNode editor.y.inline(type: string, options?: ElementStructureOptions): SlateElement ``` * **`editor.y(type, options)`** — Block element: `type` (e.g. `'paragraph'`, `'heading-one'`), optional `options.props`, `options.children`. * **`editor.y.text(text, marks)`** — Text node with optional marks (e.g. `{ bold: true }`). * **`editor.y.inline(type, options)`** — Inline element (e.g. `'link'` with `props.url`). *** ### Path * **`editor.path`** — Current path state: `{ current, selected?, selection?, source? }`. * **`editor.setPath(path)`** — Updates path; e.g. `editor.setPath({ current: 2 })`. *** ### Focus * **`editor.focus()`** — Focuses the editor. * **`editor.blur(options?)`** — Removes focus. * **`editor.isFocused()`** — Returns whether the editor is focused. *** ### Events * **`editor.on(event, callback)`** — Subscribe to event. * **`editor.off(event, callback)`** — Unsubscribe. * **`editor.once(event, callback)`** — Subscribe once. * **`editor.emit(event, payload)`** — Emit event. **Events:** `'change'` (payload: `{ operations, value }`), `'focus'`, `'blur'`, `'block:copy'`, `'path-change'`, `'plugin:register'` (payload: `{ type }`), `'plugin:unregister'` (payload: `{ type }`). *** ### Parsers (serialization) Convert content to different formats. Each parser takes the content to serialize (usually `editor.children` or the value from `onChange`). ```typescript theme={null} editor.getHTML(content: YooptaContentValue): string editor.getMarkdown(content: YooptaContentValue): string editor.getPlainText(content: YooptaContentValue): string editor.getEmail(content: YooptaContentValue, options?: Partial): string editor.getYooptaJSON(content: YooptaContentValue): string ``` **Parameters:** * `content` — `YooptaContentValue` to serialize (required). * `getEmail` only: `options` — Optional email template options. **Returns:** String in the requested format. ```typescript theme={null} const html = editor.getHTML(editor.children); const md = editor.getMarkdown(editor.children); const json = editor.getYooptaJSON(editor.children); ``` *** ### History (undo/redo) * **`editor.undo(options?)`** — Undo last batch. `options.scroll` — scroll to block; default: `true`. * **`editor.redo(options?)`** — Redo last undone batch. Same `options`. * **`editor.historyStack`** — `{ undos: HistoryStack[], redos: HistoryStack[] }` (internal). * **`editor.isSavingHistory()`** — Whether history is currently being saved. * **`editor.isMergingHistory()`** — Whether history is being merged. * **`editor.withoutSavingHistory(fn)`** — Run `fn` without pushing to history. * **`editor.withSavingHistory(fn)`** — Run `fn` with history saving enabled. * **`editor.withoutMergingHistory(fn)`** — Run `fn` without merging with previous batch. * **`editor.withMergingHistory(fn)`** — Run `fn` with history merging enabled. *** ### Batch operations and transforms * **`editor.batchOperations(callback)`** — Run `callback`; all operations performed inside are collected and applied in a single transform. Use for multiple block/element changes that should be one undo step. **Parameters:** * `callback` — Function with no args; call block/element/transform methods inside it. ```typescript theme={null} editor.batchOperations(() => { editor.insertBlock('Paragraph', { at: 0 }); editor.insertBlock('Paragraph', { at: 1 }); }); ``` * **`editor.applyTransforms(operations, options?)`** — Low-level: apply an array of Yoopta operations. Prefer block/element APIs and `batchOperations` in app code. *** ### Instance properties (read-only / internal) * **`editor.id`** — Editor ID. * **`editor.readOnly`** — Whether the editor is read-only. * **`editor.formats`** — Built mark format helpers (from `marks`). * **`editor.marks`** — Array of mark definitions passed to `createYooptaEditor`. * **`editor.plugins`** — Resolved plugin map. * **`editor.blockEditorsMap`** — Map of block ID to Slate editor (internal). * **`editor.refElement`** — DOM element ref when used with the editor component. *** ### Runtime plugin management #### registerPlugin Registers a plugin at runtime into an already-initialized editor. Useful for dynamically adding block types — loading from a marketplace, enabling optional plugins based on user settings, or lazy-loading heavy plugins. ```typescript theme={null} editor.registerPlugin(plugin: YooptaPlugin>): void ``` **Parameters:** * `plugin` — A `YooptaPlugin` instance to register (required). **Behavior:** * If a plugin with the same type is already registered, the call is a **no-op** (silently skipped). * The full plugin map is rebuilt, including element injection resolution (`injectElementsFromPlugins`). * Emits `plugin:register` event with `{ type: string }`. ```typescript theme={null} import Table from '@yoopta/table'; import Code from '@yoopta/code'; // Register a single plugin editor.registerPlugin(Table); // Listen for registration editor.on('plugin:register', ({ type }) => { console.log(`Plugin "${type}" registered`); }); ``` #### unregisterPlugin Removes a plugin at runtime. All blocks of that plugin type are deleted from content. ```typescript theme={null} editor.unregisterPlugin(pluginType: string): void ``` **Parameters:** * `pluginType` — Plugin type name, PascalCase (required). E.g. `'Table'`, `'Code'`. **Behavior:** * If no plugin with the given type is registered, the call is a **no-op**. * **All blocks of that type are removed** from `editor.children` and their Slate editors are cleaned up. This is destructive and cannot be undone via `editor.undo()`. * The plugin map is rebuilt without the removed plugin. * Emits `plugin:unregister` event with `{ type: string }`. ```typescript theme={null} // Remove a plugin editor.unregisterPlugin('Table'); // Toggle plugin on/off import Code from '@yoopta/code'; function toggleCodePlugin(enabled: boolean) { if (enabled) { editor.registerPlugin(Code); } else { editor.unregisterPlugin('Code'); } } // Listen for removal editor.on('plugin:unregister', ({ type }) => { console.log(`Plugin "${type}" removed`); }); ``` # Elements API Source: https://docs.yoopta.dev/api-reference/elements Element-level operations within blocks for Yoopta Editor # Elements API The Elements API provides element-level operations inside blocks: insert, update, delete, and query. It sits below the Blocks API and works on nodes within a block’s content. ## Accessing the API Import the `Elements` namespace from `@yoopta/editor`: ```typescript theme={null} import { Elements } from '@yoopta/editor'; Elements.insertElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', at: 'end' }); Elements.getElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: [0, 1] }); Elements.deleteElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: [0, 1] }); ``` The same functions are exported by name (`insertElement`, `getElement`, …). The `YooEditor` instance also exposes them without the first `editor` argument (e.g. `editor.insertElement({ ... })`). ## Type Definitions ```typescript theme={null} type ElementPath = number[] | 'selection' | 'first' | 'last'; ``` *** ## Methods ### insertElement Creates and inserts a new element into a block. ```typescript theme={null} Elements.insertElement(editor: YooEditor, options: InsertElementOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type, e.g. `'accordion-list-item'`, `'link'` (required). * `options.props` — Properties for the new element; merged with plugin defaults. * `options.children` — Child elements; if omitted, plugin defaults are used. * `options.at` — Where to insert: `'next'` | `'prev'` | `'start'` | `'end'` or `number[]`; default: selection or `[0]`. * `options.focus` — Focus editor after insert; default: `false`. * `options.select` — Select the inserted element; default: `false`. * `options.text` — Text for inline elements (e.g. link); use `children` for block elements. * `options.match` — Custom matcher when using `at: 'next'` or `'prev'`. ```typescript theme={null} Elements.insertElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', props: { isExpanded: true }, at: 'end', focus: true, }); ``` *** ### updateElement Updates properties (and optionally text) of an existing element. ```typescript theme={null} Elements.updateElement(editor: YooEditor, options: UpdateElementOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type to update (required). * `options.props` — Props to set; merged with existing. * `options.text` — New text for inline elements (link, mention); ignored for block elements. * `options.path` — Slate path; if omitted, uses selection (inline) or root (block). * `options.match` — Custom matcher to find the element. ```typescript theme={null} Elements.updateElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', props: { isExpanded: true }, path: [0, 1], }); ``` *** ### deleteElement Removes an element or unwraps it (inline). ```typescript theme={null} Elements.deleteElement(editor: YooEditor, options: DeleteElementOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type to delete (required). * `options.path` — Slate path; if omitted, uses selection (inline) or root (block). * `options.match` — Custom matcher to find the element. * `options.mode` — For inline only: `'unwrap'` (default) or `'remove'`. Block elements are always fully removed. ```typescript theme={null} Elements.deleteElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: [0, 1], }); ``` *** ### getElement Returns a single element by type, path, or matcher. ```typescript theme={null} Elements.getElement(editor: YooEditor, options: GetElementOptions): SlateElement | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type to find. * `options.path` — Path: `number[]` or `'selection'` | `'first'` | `'last'`; default: selection or `[0]`. * `options.match` — Custom matcher; overrides type-based search. **Returns:** Matching element or `null`. ```typescript theme={null} const item = Elements.getElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: [0, 1], }); ``` *** ### getElements Returns all elements of a type or matching a condition. ```typescript theme={null} Elements.getElements(editor: YooEditor, options: GetElementsOptions): SlateElement[] ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type to find. * `options.match` — Custom matcher; use instead of or with type. **Returns:** Array of elements; empty if none found. ```typescript theme={null} const items = Elements.getElements(editor, { blockId: 'accordion-1', type: 'accordion-list-item', }); ``` *** ### getElementEntry Returns element and its Slate path (`NodeEntry`). ```typescript theme={null} Elements.getElementEntry( editor: YooEditor, options: GetElementEntryOptions ): NodeEntry | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type. * `options.path` — Path: `number[]` or `'selection'` | `'first'` | `'last'`; default: selection or `[0]`. **Returns:** `[element, path]` or `null`. ```typescript theme={null} const entry = Elements.getElementEntry(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: [0, 1], }); if (entry) { const [element, path] = entry; } ``` *** ### getElementPath Returns the Slate path of an element instance. ```typescript theme={null} Elements.getElementPath(editor: YooEditor, options: GetElementPathOptions): Path | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.element` — The element instance (required). **Returns:** Path or `null`. ```typescript theme={null} const item = Elements.getElement(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: 'first', }); if (item) { const path = Elements.getElementPath(editor, { blockId: 'accordion-1', element: item }); } ``` *** ### getElementRect Returns the DOM bounding box for a Slate element (`domRect` and `clientRects`). ```typescript theme={null} Elements.getElementRect( editor: YooEditor, options: GetElementRectOptions ): { domRect: DOMRect; clientRects: DOMRectList } | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.element` — Slate element instance (required). **Returns:** Rects for the DOM node, or `null` if not resolved. ```typescript theme={null} const item = Elements.getElement(editor, { blockId: 'image-1', type: 'image', path: [0] }); if (item) { const rect = Elements.getElementRect(editor, { blockId: 'image-1', element: item }); if (rect) { const { width, height } = rect.domRect; } } ``` *** ### getParentElementPath Returns the Slate path of an element’s parent. ```typescript theme={null} Elements.getParentElementPath(editor: YooEditor, options: GetElementPathOptions): Path | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.element` — The element instance (required). **Returns:** Parent path or `null`. ```typescript theme={null} const parentPath = Elements.getParentElementPath(editor, { blockId: 'accordion-1', element: contentElement, }); ``` *** ### getElementChildren Returns the children array of an element. ```typescript theme={null} Elements.getElementChildren( editor: YooEditor, options: GetElementChildrenOptions ): SlateElement['children'] | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type (required). * `options.path` — Path to the element; default: selection or `[0]`. **Returns:** Children array or `null`. ```typescript theme={null} const children = Elements.getElementChildren(editor, { blockId: 'accordion-1', type: 'accordion-list-item', path: [0, 1], }); ``` *** ### getRootElement Returns the root element definition for a given block type (plugin). Each plugin defines one or more elements — the root is either the only element in the map, or the one marked with `asRoot: true`. ```typescript theme={null} Elements.getRootElement(editor: YooEditor, options: GetRootElementOptions): PluginElement | undefined ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockType` — Plugin type name, PascalCase (required). E.g. `'Paragraph'`, `'Accordion'`, `'Image'`. **Returns:** The root `PluginElement` definition, or `undefined` if the plugin doesn't exist, has no elements, or no element is marked as root in a multi-element plugin. ```typescript theme={null} // Get root element of a single-element plugin const root = Elements.getRootElement(editor, { blockType: 'Paragraph' }); // => { render: ..., props: { nodeType: 'block' } } // Check if a plugin's root element is void const imageRoot = Elements.getRootElement(editor, { blockType: 'Image' }); if (imageRoot?.props?.nodeType === 'void') { // handle void element } // Multi-element plugin — returns the element with asRoot: true const accordionRoot = Elements.getRootElement(editor, { blockType: 'Accordion' }); if (accordionRoot?.children) { console.log('Child element types:', accordionRoot.children); } // Filter plugins by root element node type const blockPlugins = Object.keys(editor.plugins).filter((type) => { const root = Elements.getRootElement(editor, { blockType: type }); const nodeType = root?.props?.nodeType; return nodeType !== 'inline' && nodeType !== 'inlineVoid'; }); ``` *** ### isElementEmpty Checks whether an element has no text content. ```typescript theme={null} Elements.isElementEmpty(editor: YooEditor, options: IsElementEmptyOptions): boolean ``` **Parameters:** * `editor` — Editor instance (required). * `options.blockId` — ID of the block (required). * `options.type` — Element type (required). * `options.path` — Path to the element. **Returns:** `true` if empty, `false` otherwise. ```typescript theme={null} const isEmpty = Elements.isElementEmpty(editor, { blockId: 'accordion-1', type: 'accordion-list-item-content', path: [0, 1, 1], }); ``` # Marks API Source: https://docs.yoopta.dev/api-reference/marks Text formatting (marks) API for Yoopta Editor # Marks API The Marks API lets you apply, update, remove, and query text formatting (bold, italic, color, highlight, etc.) in the editor. Marks are applied to the current selection or to specific blocks. Use the `Marks` namespace or `createYooptaMark` to define custom marks. ## Accessing the API Import the `Marks` namespace and `createYooptaMark` from `@yoopta/editor`: ```typescript theme={null} import { Marks, createYooptaMark } from '@yoopta/editor'; // Apply/toggle/query marks Marks.toggle(editor, { type: 'bold' }); Marks.add(editor, { type: 'color', value: '#ff0000' }); const isBold = Marks.isActive(editor, { type: 'bold' }); // Define custom mark (passed to createYooptaEditor) const highlightMark = createYooptaMark({ type: 'highlight', hotkey: 'mod+shift+h', render: (props) => {props.children}, }); ``` Marks API methods are not on the editor instance — use the `Marks` namespace. Marks must be registered via `createYooptaEditor({ marks: [...] })`. ## Type Definitions ```typescript theme={null} import type { Range } from 'slate'; type YooptaPathIndex = number; // Options for methods that target selection or blocks type MarkTargetOptions = { at?: YooptaPathIndex | YooptaPathIndex[]; blockId?: string | string[]; selection?: Range; // Slate Range; when set, at/blockId are ignored }; // Custom mark definition (for createYooptaEditor) type YooptaMark = { type: string; hotkey?: string; render: (props: TProps) => JSX.Element; }; ``` *** ## Methods ### update Sets the value of a mark in the current selection or in specified blocks. ```typescript theme={null} Marks.update(editor: YooEditor, options: UpdateMarkOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.type` — Mark type, e.g. `'color'`, `'fontSize'` (required). * `options.value` — Value to set (required); e.g. `'#ff0000'`, `16`. * `options.at` — Block index or array of indices; default: current selection path or selected blocks. * `options.blockId` — Block ID or array of IDs. * `options.selection` — Slate `Range`; when set, applies to this range instead of blocks. ```typescript theme={null} Marks.update(editor, { type: 'color', value: '#ff0000', }); Marks.update(editor, { type: 'fontSize', value: 16, at: 0, }); ``` *** ### add Adds a mark (or sets its value) in the current selection or in specified blocks. Alias: `Marks.add` (same as `addMark`). ```typescript theme={null} Marks.add(editor: YooEditor, options: AddMarkOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.type` — Mark type (required). * `options.value` — Value to set (required); e.g. `true` for bold, `'#ff0000'` for color. * `options.at` — Block index or array of indices. * `options.blockId` — Block ID or array of IDs. * `options.selection` — Slate `Range` to apply to. ```typescript theme={null} Marks.add(editor, { type: 'bold', value: true }); Marks.add(editor, { type: 'color', value: '#ff0000', selection: { anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: 5 } }, }); ``` *** ### remove Removes a mark from the current selection or from specified blocks. Alias: `Marks.remove` (same as `removeMark`). ```typescript theme={null} Marks.remove(editor: YooEditor, options: RemoveMarkOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.type` — Mark type to remove (required). * `options.at` — Block index or array of indices. * `options.blockId` — Block ID or array of IDs. * `options.selection` — Slate `Range` to remove from. ```typescript theme={null} Marks.remove(editor, { type: 'bold' }); Marks.remove(editor, { type: 'color', at: 0 }); ``` *** ### toggle Toggles a mark in the current selection or in specified blocks (adds if absent, removes if present). ```typescript theme={null} Marks.toggle(editor: YooEditor, options: ToggleMarkOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.type` — Mark type to toggle (required). * `options.at` — Block index or array of indices. * `options.blockId` — Block ID or array of IDs. * `options.selection` — Slate `Range` to toggle in. ```typescript theme={null} Marks.toggle(editor, { type: 'bold' }); Marks.toggle(editor, { type: 'italic', at: 0 }); Marks.toggle(editor, { type: 'underline', at: [0, 1, 2] }); ``` *** ### isActive Returns whether a mark is active in the current selection or in a given block. ```typescript theme={null} Marks.isActive(editor: YooEditor, options: IsMarkActiveOptions): boolean ``` **Parameters:** * `editor` — Editor instance (required). * `options.type` — Mark type to check (required). * `options.at` — Block index; default: current selection path. * `options.blockId` — Block ID. **Returns:** `true` if the mark is active, `false` otherwise. ```typescript theme={null} const isBold = Marks.isActive(editor, { type: 'bold' }); const isItalic = Marks.isActive(editor, { type: 'italic', at: 0 }); ``` *** ### getValue Returns the value of a mark in the current selection or in a given block. ```typescript theme={null} Marks.getValue(editor: YooEditor, options: GetMarkValueOptions): unknown | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.type` — Mark type (required). * `options.at` — Block index; default: current selection path. * `options.blockId` — Block ID. **Returns:** Mark value or `null` if not set. ```typescript theme={null} const boldValue = Marks.getValue(editor, { type: 'bold' }); const color = Marks.getValue(editor, { type: 'color', at: 0 }); ``` *** ### getAll Returns all marks in the current selection or in a given block. Alias: `Marks.getAll` (same as `getMarks`). ```typescript theme={null} Marks.getAll(editor: YooEditor, options?: GetMarksOptions): Record | null ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Block index; default: current selection path. * `options.blockId` — Block ID. **Returns:** Object of mark type → value, or `null` if no Slate context. ```typescript theme={null} const marks = Marks.getAll(editor); const marksInBlock = Marks.getAll(editor, { at: 0 }); ``` *** ### clear Removes all marks from the current selection or from specified blocks. ```typescript theme={null} Marks.clear(editor: YooEditor, options?: ClearMarksOptions): void ``` **Parameters:** * `editor` — Editor instance (required). * `options.at` — Block index or array of indices; when omitted and no `selection`, uses current selection or selected blocks. * `options.blockId` — Block ID or array of IDs. * `options.selection` — Slate `Range` to clear. ```typescript theme={null} Marks.clear(editor); Marks.clear(editor, { at: 0 }); Marks.clear(editor, { at: [0, 1, 2] }); Marks.clear(editor, { selection: { anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: 5 } }, }); ``` *** ## Defining custom marks: createYooptaMark Use `createYooptaMark` to define a mark that you then pass to `createYooptaEditor({ marks: [...] })`. The mark has a `type`, optional `hotkey`, and a `render` function. ```typescript theme={null} createYooptaMark(params: YooptaMarkParams): YooptaMark ``` **Parameters:** * `params.type` — Unique mark type string (required). * `params.hotkey` — Optional keyboard shortcut, e.g. `'mod+b'` for bold. * `params.render` — React component that receives Slate render props and renders the marked text (required). **Returns:** A `YooptaMark` object to add to the `marks` array when creating the editor. ```typescript theme={null} import { createYooptaMark } from '@yoopta/editor'; const highlightMark = createYooptaMark({ type: 'highlight', hotkey: 'mod+shift+h', render: (props) => ( {props.children} ), }); // In createYooptaEditor const editor = createYooptaEditor({ plugins: [...], marks: [highlightMark, ...defaultMarks], }); ``` # Collaboration Source: https://docs.yoopta.dev/core/collaboration Real-time collaboration for Yoopta Editor using Yjs CRDT ## Overview The `@yoopta/collaboration` package enables real-time collaborative editing in Yoopta Editor. Multiple users can edit the same document simultaneously with automatic conflict resolution powered by [Yjs](https://yjs.dev/) CRDT. The package provides: * **`withCollaboration`** extension — connects your editor to a collaboration server * **Collaborative undo/redo** — Cmd+Z only undoes your own changes, not other users' * **Remote cursors** — see where other users are editing * **Awareness** — track connected users and their presence * **React hooks** — access collaboration state in your components The collaboration client is **fully open-source** and works with any Yjs-compatible WebSocket server. You can self-host with your own infrastructure or use [Yoopta Cloud](#yoopta-cloud) for a managed solution with version history, permissions, and more. ## Installation ```bash theme={null} npm install @yoopta/collaboration ``` `@yoopta/collaboration` requires `@yoopta/editor` >= 6.0.0 and React >= 18.2.0 as peer dependencies. ## Quick Start Wrap your editor with `withCollaboration` — similar to how `withMentions` works: ```tsx theme={null} import { useMemo, useEffect } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import { withCollaboration, RemoteCursors } from '@yoopta/collaboration'; import { PLUGINS } from './plugins'; import { MARKS } from './marks'; export default function CollaborativeEditor() { const editor = useMemo( () => withCollaboration(createYooptaEditor({ plugins: PLUGINS, marks: MARKS }), { url: 'wss://collab.yoopta.cloud', roomId: 'document-123', token: 'your-auth-token', user: { id: 'user-1', name: 'Alice', color: '#e06c75', }, }), [], ); useEffect(() => { editor.collaboration.connect(); return () => { editor.collaboration.disconnect(); }; }, [editor.collaboration]); return ( console.log(value)}> {/* Your other UI components */} ); } ``` Always call `editor.collaboration.destroy()` on cleanup to properly close the WebSocket connection and remove awareness states. ## Configuration ### `withCollaboration(editor, config)` Extends a `YooEditor` instance with collaboration capabilities. Returns a `CollaborationYooEditor`. The editor instance from `createYooptaEditor()`. Configuration object for the collaboration session. ### CollaborationConfig WebSocket server URL. Example: `"wss://collab.yoopta.cloud"` Unique identifier for the document/room. All users with the same `roomId` will collaborate on the same document. Information about the current user. Used for cursor labels and presence indicators. ```ts theme={null} { id: string; // Unique user identifier name: string; // Display name (shown on cursors) color: string; // Cursor color (hex, e.g. "#e06c75") avatar?: string; // Optional avatar URL } ``` Authentication token sent to the server on connection. Use this to verify users and check subscription access on the server side. Initial content to seed the document if no remote state exists. When the first user connects to an empty room, this value populates the shared document. Whether to connect to the server immediately. Set to `false` to connect manually later via `editor.collaboration.connect()`. Optional: provide your own Yjs `Y.Doc` instance. If not provided, one is created automatically. Useful for advanced integrations or testing. ## Editor API After applying `withCollaboration`, the editor gains an `editor.collaboration` namespace: ### `editor.collaboration.state` Read-only access to the current collaboration state. ```ts theme={null} type CollaborationState = { status: ConnectionStatus; // 'disconnected' | 'connecting' | 'connected' | 'error' connectedUsers: CollaborationUser[]; // All users currently in the room document: Y.Doc | null; // The Yjs document instance isSynced: boolean; // Whether initial sync with server is complete }; ``` ### `editor.collaboration.connect()` Manually connect to the WebSocket server. Only needed if `connect: false` was passed in config. ```ts theme={null} editor.collaboration.connect(); ``` ### `editor.collaboration.disconnect()` Disconnect from the server. The editor remains usable for local editing. ```ts theme={null} editor.collaboration.disconnect(); ``` ### `editor.collaboration.destroy()` Disconnect and clean up all resources (WebSocket, observers, awareness). Call this on component unmount. ```ts theme={null} useEffect(() => { return () => editor.collaboration.destroy(); }, [editor]); ``` ### `editor.collaboration.getDocument()` Returns the underlying `Y.Doc` instance for advanced use cases. ```ts theme={null} const doc = editor.collaboration.getDocument(); ``` ## Components ### `` Renders visual indicators showing which block each remote user is currently editing. ```tsx theme={null} import { RemoteCursors } from '@yoopta/collaboration'; ; ``` Must be rendered as a child of ``. Displays a colored bar and name label next to the block each remote user is editing. ## Hooks All hooks must be used within a `` component that has collaboration enabled. ### `useCollaboration()` Returns the full collaboration state. Re-renders on any state change. ```tsx theme={null} import { useCollaboration } from '@yoopta/collaboration'; function StatusBar() { const { status, connectedUsers, isSynced } = useCollaboration(); return (
{status} {connectedUsers.length} online {!isSynced && Syncing...}
); } ``` ### `useConnectionStatus()` Returns only the connection status string. Lighter than `useCollaboration()` when you only need the status. ```tsx theme={null} import { useConnectionStatus } from '@yoopta/collaboration'; function ConnectionIndicator() { const status = useConnectionStatus(); return (
); } ``` ### `useRemoteCursors()` Returns an array of remote cursor data for all connected users. ```tsx theme={null} import { useRemoteCursors } from '@yoopta/collaboration'; function UserPresence() { const cursors = useRemoteCursors(); return (
{cursors.map((cursor) => (
{cursor.user.name} - editing block {cursor.blockId}
))}
); } ``` ## Deferred Connection If you need to connect after the editor is rendered (e.g., after authentication), set `connect: false`: ```tsx theme={null} const editor = useMemo( () => withCollaboration(createYooptaEditor({ plugins: PLUGINS, marks: MARKS }), { url: 'wss://collab.yoopta.cloud', roomId: 'doc-123', user: { id: 'user-1', name: 'Alice', color: '#e06c75' }, connect: false, // Don't connect yet }), [], ); // Connect later, e.g. after auth function handleLogin(token: string) { editor.collaboration.connect(); } ``` ## Custom Y.Doc For advanced use cases (e.g., testing two editors in the same page), you can provide your own `Y.Doc`: ```tsx theme={null} import * as Y from 'yjs'; const doc = new Y.Doc(); const editor = useMemo( () => withCollaboration(createYooptaEditor({ plugins: PLUGINS, marks: MARKS }), { url: 'wss://collab.yoopta.cloud', roomId: 'doc-123', user: { id: 'user-1', name: 'Alice', color: '#e06c75' }, document: doc, }), [], ); ``` ## Self-Hosted Setup `@yoopta/collaboration` uses Yjs only as a **sync layer** — it does not store your documents. Your content remains standard Yoopta JSON (`YooptaContentValue`) that you store in your own database. ``` ┌─────────────┐ load ┌──────────────────┐ sync ┌─────────────┐ │ Your DB │ ──────────────►│ Yoopta Editor │◄───────────────►│ Other │ │ (Postgres, │ initialValue │ + Y.Doc │ WebSocket │ Clients │ │ Mongo, ..) │◄──────────────┤ │ (y-protocols) │ │ └─────────────┘ save on └────────┬───────────┘ └─────────────┘ change │ │ ┌──────▼──────────┐ │ Your WebSocket │ │ Server │ └─────────────────┘ ``` The Y.Doc is **ephemeral** — it only lives during the collaboration session. When all users disconnect, it can be discarded. Your database is the source of truth. ### Persisting to Your Database Load content from your database as `initialValue`, and save back on change: ```tsx theme={null} import { useMemo, useEffect, useCallback } from 'react'; import { debounce } from 'lodash'; // or your preferred debounce import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import { withCollaboration, RemoteCursors } from '@yoopta/collaboration'; export default function CollaborativeEditor({ documentId, savedContent, currentUser }) { const editor = useMemo( () => withCollaboration(createYooptaEditor({ plugins: PLUGINS, marks: MARKS }), { url: 'ws://localhost:4000', // Your own server roomId: documentId, user: currentUser, initialValue: savedContent, // Load from your DB }), [], ); // Save to your database on change const handleChange = useCallback( debounce((value) => { fetch(`/api/documents/${documentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(value), }); }, 2000), [documentId], ); useEffect(() => { editor.collaboration.connect(); return () => { editor.collaboration.disconnect(); }; }, [editor.collaboration]); return ( ); } ``` The `initialValue` is only used when the first user connects to an empty room. Once the Y.Doc has content (from any client), subsequent connections load from the Y.Doc state, not from `initialValue`. ### Running Your Own WebSocket Server Any Yjs-compatible WebSocket server works. The simplest option is [y-websocket](https://github.com/yjs/y-websocket): ```bash theme={null} npm install y-websocket ``` ```js theme={null} // server.js import { WebSocketServer } from 'ws'; import { setupWSConnection } from 'y-websocket/bin/utils'; const wss = new WebSocketServer({ port: 4000 }); wss.on('connection', (ws, req) => { // req.url is "/" setupWSConnection(ws, req); }); console.log('Yjs WebSocket server running on ws://localhost:4000'); ``` For production, consider [Hocuspocus](https://tiptap.dev/hocuspocus) which adds authentication, persistence hooks, and webhook support out of the box. For small teams (2-5 concurrent editors), a single y-websocket process is more than sufficient. No special infrastructure needed. ## Yoopta Cloud For teams that don't want to run their own collaboration infrastructure, **Yoopta Cloud** provides a managed solution: * Managed WebSocket infrastructure with automatic scaling * Built-in document persistence and version history * Authentication and room-level permissions * Comments and threads * REST API for document access * Webhooks for integrations (search indexing, backups, workflows) * Analytics dashboard ```tsx theme={null} // Using Yoopta Cloud — just change the URL and add your API key const editor = useMemo( () => withCollaboration(createYooptaEditor({ plugins: PLUGINS, marks: MARKS }), { url: 'wss://collab.yoopta.cloud', roomId: 'document-123', token: 'your-api-key', user: { id: 'user-1', name: 'Alice', color: '#e06c75' }, }), [], ); ``` Yoopta Cloud is coming soon. The self-hosted path described above will always remain free and open-source. ## How It Works Under the hood, `@yoopta/collaboration` maintains a bidirectional binding between the Yoopta editor and a Yjs shared document: ``` ┌──────────────────────┐ ┌──────────────────────┐ │ Yoopta Editor │ │ Y.Doc │ │ │ │ │ │ editor.children ◄──►│ binding│ Y.Array("blockOrder")│ │ block ordering │◄──────►│ Y.Map("blockMeta") │ │ block metadata │ │ Y.Map("blockContents")│ │ Slate content │ │ │ └──────────────────────┘ └──────┬───────────────┘ │ │ y-protocols (WebSocket) │ ┌──────▼───────────────┐ │ Collab Server │ └───────────────────────┘ ``` * **Local changes** flow through `editor.applyTransforms` and are automatically synced to the Y.Doc, then sent to the server via WebSocket. * **Remote changes** arrive via WebSocket, update the Y.Doc, and are applied to the editor without entering the local undo/redo history. * **Conflict resolution** is handled by the Yjs CRDT — concurrent edits are merged automatically at the character level. * **Undo/redo** uses Yjs `UndoManager` to selectively reverse only local changes (see below). ## Undo / Redo When collaboration is active, `editor.undo()` and `editor.redo()` (Cmd+Z / Cmd+Shift+Z) automatically use Yjs `UndoManager` instead of the default history stack. This means: * **Only your changes are undone** — if Alice types "Hello" and Bob types "World", Alice pressing Cmd+Z removes only "Hello". Bob's text stays intact. * **Works at all levels** — text edits, block insertions, deletions, splits, merges, formatting, and metadata changes are all tracked. * **No extra setup needed** — `withCollaboration` overrides undo/redo automatically. When collaboration is destroyed, the default history behavior is restored. Rapid edits (e.g., continuous typing) are batched into a single undo step, similar to how undo works in Google Docs or Notion. The initial document content (loaded from the server or seeded via `initialValue`) is not undoable — only edits made after the document loads can be undone. ## TypeScript All types are exported from the package: ```ts theme={null} import type { CollaborationConfig, CollaborationUser, CollaborationState, ConnectionStatus, RemoteCursorData, CollaborationAPI, CollaborationYooEditor, } from '@yoopta/collaboration'; ``` ## WebSocketProvider For advanced server integrations, the `WebSocketProvider` class is exported: ```ts theme={null} import { WebSocketProvider } from '@yoopta/collaboration'; ``` This is the transport layer used internally. It implements the Yjs sync and awareness protocols over WebSocket with automatic reconnection and authentication support. # Editor Source: https://docs.yoopta.dev/core/editor Understanding the Yoopta Editor core component ## Overview The Yoopta Editor is the main component that orchestrates all plugins, marks, and UI components. You create an editor instance with `createYooptaEditor()` (passing plugins and optional marks/value there), then render ``. Content is stored on the editor instance and synced via `onChange`. ## Creating an Editor Instance Use `createYooptaEditor()` with an options object. **Plugins are required**; marks and initial value are optional: ```jsx theme={null} import { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; const editor = useMemo( () => createYooptaEditor({ plugins: [Paragraph], marks: [], // optional, e.g. from @yoopta/marks value: undefined, // optional initial content id: undefined, // optional editor ID readOnly: false, }), [], ); ``` Always wrap `createYooptaEditor()` in `useMemo` (with stable `plugins`/`marks`) to prevent recreating the editor on every render. Plugins and marks are configured at creation time, not on the `` component. Value is managed via the editor instance and `onChange`. ## YooptaEditor Component Props The `` component accepts: ### Required The editor instance from `createYooptaEditor()` ### Optional Called when content changes. Use it to sync `value` to your state (e.g. for saving or controlled usage). Called when the selection path changes (current block, selected blocks, etc.). Whether to focus the editor on mount. Global placeholder text shown when the editor has a single empty block. If a plugin defines its own element-level placeholder, the element-level placeholder takes priority in multi-block editors. See [Placeholders](#placeholders) for details. CSS class for the editor wrapper. Inline styles for the editor wrapper. UI components to render inside the editor (e.g. toolbar, slash menu, block actions). Custom wrapper for each block (e.g. for drag-and-drop). ## Basic Example ```jsx theme={null} import { useMemo, useState } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; const plugins = [Paragraph]; export default function MyEditor() { const editor = useMemo( () => createYooptaEditor({ plugins, value: undefined, }), [], ); const [value, setValue] = useState(editor.children); const handleChange = (newValue, options) => { setValue(newValue); console.log('Operations:', options.operations); }; return ( ); } ``` ## Placeholders Yoopta supports placeholders at two levels that work together: * **Editor-level** — a global fallback placeholder passed to `` * **Element-level** — per-plugin placeholders set via `.extend()` on each element type ### How resolution works When a block is focused and its element is empty, the editor resolves which placeholder to display: 1. Check the focused element's type (e.g. `heading-one`, `step-list-item-heading`) 2. If that element has a `placeholder` defined — use it 3. Otherwise, fall back to the editor-level `placeholder` prop This means element-level placeholders always take priority over the global one. ### Editor-level placeholder ```jsx theme={null} ``` ### Element-level placeholder Set placeholders per element type using `.extend()`: ```jsx theme={null} import { HeadingOne } from '@yoopta/headings'; import Paragraph from '@yoopta/paragraph'; const plugins = [ Paragraph.extend({ elements: { paragraph: { placeholder: 'Type something...', }, }, }), HeadingOne.extend({ elements: { 'heading-one': { placeholder: 'Heading 1', }, }, }), ]; ``` ### Nested plugins For plugins with multiple nested elements (e.g. Steps, Accordion), you can set a different placeholder for each element: ```jsx theme={null} import Steps from '@yoopta/steps'; const plugins = [ Steps.extend({ elements: { 'step-list-item-heading': { placeholder: 'Step title', }, 'step-list-item-content': { placeholder: 'Describe this step...', }, }, }), ]; ``` Each nested element resolves its own placeholder independently — the heading shows "Step title" while the content area shows "Describe this step...". ### Styling Placeholders are rendered via the `.yoopta-placeholder` CSS class and `data-placeholder` attribute. If you use a Yoopta theme (e.g. `@yoopta/themes-shadcn`), placeholder styles are included. For headless usage, add your own styles: ```css theme={null} .yoopta-placeholder { position: relative; } .yoopta-placeholder::before { content: attr(data-placeholder); position: absolute; top: 0; left: 0; pointer-events: none; opacity: 0.4; white-space: nowrap; user-select: none; } ``` ## Content Value Structure Content is a record of block ID → block data. Type names use PascalCase for block types (e.g. `Paragraph`), and kebab-case for element types inside `value`. ```typescript theme={null} type YooptaContentValue = Record; type YooptaBlockData = { id: string; type: string; // e.g. 'Paragraph', 'HeadingOne' meta: { order: number; depth: number; align?: 'left' | 'center' | 'right'; }; value: SlateElement[]; // Slate nodes (paragraph, heading-one, etc.) }; ``` ### Example ```json theme={null} { "block-1": { "id": "block-1", "type": "Paragraph", "meta": { "order": 0, "depth": 0 }, "value": [ { "id": "el-1", "type": "paragraph", "children": [{ "text": "Hello World!" }] } ] } } ``` ## onChange Options The `onChange` callback receives the new content and an options object: ```typescript theme={null} type YooptaOnChangeOptions = { operations: YooptaOperation[]; // List of operations that caused the change }; ``` Use `options.operations` to know what changed (insert, update, delete, move, etc.) without diffing the whole value. ## Styling the Editor ### Using className ```jsx theme={null} ``` ```css theme={null} .my-editor { max-width: 800px; margin: 0 auto; padding: 40px 20px; min-height: 500px; } ``` ### Using style ```jsx theme={null} ``` ## Multiple Editors Each editor instance is independent. Create separate instances with `createYooptaEditor()` and separate state for each: ```jsx theme={null} function MultipleEditors() { const editor1 = useMemo(() => createYooptaEditor({ plugins }), []); const editor2 = useMemo(() => createYooptaEditor({ plugins }), []); const [value1, setValue1] = useState(editor1.children); const [value2, setValue2] = useState(editor2.children); return ( <> ); } ``` ## Next Steps Learn about plugins and how to use them createYooptaEditor options and YooEditor instance API Handle editor events (on, off, emit) # Plugins Source: https://docs.yoopta.dev/core/plugins Understanding and working with Yoopta Editor plugins ## Overview Plugins are the building blocks of Yoopta Editor. Each plugin represents a specific type of content block (paragraph, heading, image, etc.) and defines how that content is rendered and behaves. Every plugin in Yoopta Editor is built using the `YooptaPlugin` class, which provides a consistent API for defining block types, handling events, managing state, and extending functionality. ## Plugin Architecture ### YooptaPlugin Class All plugins are created using the `YooptaPlugin` class constructor: ```typescript theme={null} import { YooptaPlugin } from '@yoopta/editor'; const MyPlugin = new YooptaPlugin({ type: 'MyPlugin', elements: { // Plugin elements definition }, options: { // Plugin options }, commands: { // Plugin commands }, events: { // Event handlers }, lifecycle: { // Lifecycle hooks }, parsers: { // HTML, Markdown, Email parsers }, extensions: { // Slate extensions }, }); ``` ### Plugin Structure Each plugin configuration consists of: Unique identifier for the plugin (e.g., 'Paragraph', 'Image', 'Code') Defines the structure and rendering of plugin elements. Can be an object or JSX syntax Configuration options including display settings and shortcuts Methods for manipulating plugin content programmatically Event handlers for keyboard, mouse, and other DOM events Hooks that run at specific points in the plugin lifecycle Serialization and deserialization for HTML, Markdown, and Email Slate editor extensions for custom behavior ## Available Plugins Yoopta Editor provides 20+ built-in plugins: | Category | Plugins | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Text** | [Paragraph](/plugins/paragraph), [Headings](/plugins/headings) (H1, H2, H3), [Blockquote](/plugins/blockquote) | | **Lists** | [BulletedList, NumberedList, TodoList](/plugins/lists) | | **Media** | [Image](/plugins/image), [Video](/plugins/video), [File](/plugins/file), [Embed](/plugins/embed) | | **Layout** | [Table](/plugins/table), [Accordion](/plugins/accordion), [Tabs](/plugins/tabs), [Steps](/plugins/steps), [Carousel](/plugins/carousel), [Callout](/plugins/callout), [Divider](/plugins/divider) | | **Code** | [Code](/plugins/code), [CodeGroup](/plugins/code-group) | | **Inline** | [Link](/plugins/link), [Mention](/plugins/mention), [Emoji](/plugins/emoji) | | **Navigation** | [TableOfContents](/plugins/table-of-contents) | ## Using Plugins Plugins are passed to **`createYooptaEditor`**, not to ``. The component only receives the `editor` instance. ### Basic Usage ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; import Blockquote from '@yoopta/blockquote'; import Code from '@yoopta/code'; const plugins = [Paragraph, Blockquote, Code]; export default function Editor() { const editor = useMemo( () => createYooptaEditor({ plugins, marks: [] }), [], ); return ( {}} placeholder="Type / to open menu" /> ); } ``` ### Headings Plugin The Headings plugin exports multiple heading types: ```jsx theme={null} import Headings from '@yoopta/headings'; const plugins = [ Headings.HeadingOne, // H1 Headings.HeadingTwo, // H2 Headings.HeadingThree, // H3 ]; ``` ### Lists Plugin The Lists plugin exports multiple list types: ```jsx theme={null} import Lists from '@yoopta/lists'; const plugins = [Lists.BulletedList, Lists.NumberedList, Lists.TodoList]; ``` ## Plugin Configuration Most plugins accept configuration options when you call `plugin.extend({ options: { ... } })`. Pass the resulting plugins array to `createYooptaEditor({ plugins, marks })`, not to ``. ### Image Plugin ```jsx theme={null} import Image from '@yoopta/image'; const plugins = [ Image.extend({ options: { async onUpload(file) { // Custom upload logic const url = await uploadToServer(file); return { src: url }; }, maxWidth: 1000, minWidth: 100, }, }), ]; ``` ### Code Plugin ```jsx theme={null} import Code from '@yoopta/code'; const plugins = [ Code.extend({ options: { theme: 'github-dark', language: 'javascript', languages: ['javascript', 'typescript', 'python', 'rust'], }, }), ]; ``` ### Video Plugin ```jsx theme={null} import Video from '@yoopta/video'; const plugins = [ Video.extend({ options: { async onUpload(file) { const url = await uploadVideo(file); return { src: url, provider: 'custom' }; }, maxSize: 100 * 1024 * 1024, // 100MB accept: 'video/*', }, }), ]; ``` ## Plugin Methods ### extend() Extend a plugin with custom options or elements (UI): ```jsx theme={null} const CustomParagraph = Paragraph.extend({ options: { display: { title: 'Text', description: 'Plain text' }, shortcuts: ['p'], }, elements: { paragraph: { render: (props) => (

{props.children}

), }, }, }); ``` ### Defining Elements Elements can be defined in two ways: **1. Object Syntax:** ```typescript theme={null} const MyPlugin = new YooptaPlugin({ type: 'MyPlugin', elements: { 'my-element': { render: (props) =>
{props.children}
, props: { /* default props */ }, nodeType: 'block', // or 'void' or 'inline' }, }, }); ``` **2. JSX Syntax (Recommended):** ```typescript theme={null} const MyPlugin = new YooptaPlugin({ type: 'MyPlugin', elements: ( ), }); ``` ### Element Properties React component that renders the element Default props for the element Type of Slate node. Default is 'block' Placeholder text shown when this element is focused and empty. Set via `.extend()`: ```jsx theme={null} HeadingOne.extend({ elements: { 'heading-one': { placeholder: 'Heading 1' }, }, }) ``` Element-level placeholders take priority over the editor-level `placeholder` prop. For nested plugins (Steps, Accordion), each element can have its own placeholder. See [Editor Placeholders](/core/editor#placeholders) for the full guide. ## Custom elements (renders) Override default rendering by extending with custom `elements`: ```jsx theme={null} import Paragraph from '@yoopta/paragraph'; const CustomParagraph = Paragraph.extend({ elements: { paragraph: { render: (props) => (

{props.children}

), }, }, }); ``` ## Plugin Events Handle plugin-specific events: ```jsx theme={null} const CustomImage = Image.extend({ events: { onPaste: (editor, slate, options) => { return (event) => { console.log('onPaste', event); }; }, onKeyDown: (editor, slate, options) => { return (event) => { console.log('onKeyDown', event); }; }, }, }); ``` ## Plugin shortcuts Configure slash-menu shortcuts via `options.shortcuts` (array of strings). Plugin-specific hotkeys are defined in the plugin's `events.onKeyDown` or in options. Use `Blocks.toggleBlock(editor, 'Code', { preserveContent: true })` for programmatic block type change. ## Common Plugin Options Most plugins support these common options: Display configuration - `title` - Display name in UI - `description` - Plugin description - `icon` * Custom icon component Keyboard shortcuts to trigger the plugin Slate-specific configuration ## Plugin Loading Order The order of plugins in the array affects: 1. Menu display order 2. Transform priority 3. Shortcut precedence ```jsx theme={null} const plugins = [ Paragraph, // Shows first in menus Headings.HeadingOne, Headings.HeadingTwo, // ... Divider, // Shows last in menus ]; ``` ## Best Practices * Always include the **Paragraph** plugin — it's the default block type * Define your `plugins` array **outside the component** to prevent recreation on every render * Use **`extend()`** to customize plugins instead of modifying plugin objects directly * For plugins with async operations (uploads), always handle errors and loading states ## Lifecycle Hooks Lifecycle hooks allow you to run code at specific points in a block's lifecycle: ```typescript theme={null} const MyPlugin = new YooptaPlugin({ type: 'MyPlugin', lifecycle: { beforeCreate: (editor) => { // Return initial block structure return editor.y('my-element', { children: [editor.y.text('Initial content')], }); }, onCreate: (editor, block) => { // Run after block is created }, onDestroy: (editor, block) => { // Cleanup before block is destroyed }, }, }); ``` ### Available Lifecycle Hooks Called before a block is created. Return the initial structure Called after a block is created Called before a block is destroyed ## Parsers Parsers handle serialization and deserialization of content: ```typescript theme={null} const MyPlugin = new YooptaPlugin({ type: 'MyPlugin', parsers: { html: { deserialize: { nodeNames: ['DIV'], parse: (el) => { // Convert HTML element to Slate node return { type: 'my-element', children: [{ text: el.textContent }], }; }, }, serialize: (element, text, blockMeta) => { // Convert Slate node to HTML string return `
${text}
`; }, }, markdown: { serialize: (element, text) => { // Convert to Markdown return `${text}\n`; }, }, email: { serialize: (element, text) => { // Convert to email-compatible HTML return `
${text}
`; }, }, }, }); ``` ## Next Steps Learn about the basic text plugin Working with images and uploads Syntax-highlighted code blocks Create and manage tables # Themes Source: https://docs.yoopta.dev/core/themes Headless plugins and theme packages for styled block UI ## Overview The editor and all plugins are **headless** by default: they define structure and behavior but do not ship UI for how block elements look. You can keep them headless (e.g. for custom design) or attach styled UI from a **theme package**. Available themes: * **`@yoopta/themes-shadcn`** — Shadcn UI styled components (production ready) * **`@yoopta/themes-material`** — Material Design styled components (in progress) Themes provide UI components for plugin elements (e.g. Callout, Code, Image). You can either apply a theme to **all** plugins at once with `applyTheme()`, or attach theme UI to **individual** plugins via `plugin.extend({ elements: ThemeUI })`. *** ## Concept: headless + optional UI Plugins define their block type, elements, and behavior; they do not require any specific look. Theme packages export: 1. **Per-plugin UI objects** — e.g. `CalloutUI` from `@yoopta/themes-shadcn/callout`, which you pass into `Callout.extend({ elements: CalloutUI })`. 2. **`applyTheme(plugins)`** — a helper that applies the theme’s UI to every supported plugin in the array in one go. So you can: * Use plugins as-is (headless) and render elements yourself. * Use theme UI for a single plugin: `Callout.extend({ elements: CalloutUI })`. * Use theme UI for all plugins: `applyTheme([Paragraph, Callout, ...])`. *** ## Apply theme to all plugins Use `applyTheme()` from a theme package to wrap your plugin list; the theme will attach its styled elements to every plugin it supports. ```bash theme={null} npm install @yoopta/themes-shadcn ``` ```tsx theme={null} import { createYooptaEditor } from '@yoopta/editor'; import { applyTheme } from '@yoopta/themes-shadcn'; import Paragraph from '@yoopta/paragraph'; import Callout from '@yoopta/callout'; import Headings from '@yoopta/headings'; const plugins = applyTheme([ Paragraph, Callout, Headings.HeadingOne, Headings.HeadingTwo, Headings.HeadingThree, ]); const editor = createYooptaEditor({ plugins, marks: [] }); ``` Then pass `plugins` (the result of `applyTheme`) into `createYooptaEditor`. The theme’s CSS is applied by the package; you don’t pass plugins or marks to ``. *** ## Apply theme UI to a single plugin If you only want themed UI for specific plugins, import the theme’s UI object for that plugin and extend the plugin with `elements`. **Example: Callout with Shadcn UI** ```tsx theme={null} import Callout from '@yoopta/callout'; import { CalloutUI } from '@yoopta/themes-shadcn/callout'; const CalloutWithUI = Callout.extend({ elements: CalloutUI, }); ``` Use `CalloutWithUI` in your plugins array; other plugins can stay headless or use other themes. Subpaths are available per plugin, for example: * `@yoopta/themes-shadcn/callout` → `CalloutUI` * `@yoopta/themes-shadcn/paragraph` → `ParagraphUI` * `@yoopta/themes-shadcn/embed` → `EmbedUI` * …and other plugins supported by the theme *** ## @yoopta/themes-shadcn Production-ready theme based on [Shadcn UI](https://ui.shadcn.com/). It provides styled elements for paragraph, headings, lists, blockquote, callout, code, image, video, embed, file, table, tabs, steps, accordion, carousel, divider, link, mention, and table-of-contents. **Install** ```bash theme={null} npm install @yoopta/themes-shadcn ``` **Apply to all plugins** ```tsx theme={null} import { applyTheme } from '@yoopta/themes-shadcn'; const plugins = applyTheme([ Paragraph, Headings.HeadingOne, Callout, // ... ]); ``` **Apply to one plugin** ```tsx theme={null} import { CalloutUI } from '@yoopta/themes-shadcn/callout'; Callout.extend({ elements: CalloutUI }); ``` ### CSS Variables The theme reads from standard [shadcn/ui](https://ui.shadcn.com/) CSS variables (`--background`, `--foreground`, `--card`, `--border`, etc.). If your app already uses shadcn/ui, the theme works out of the box — no extra setup needed. If you **don't** have shadcn CSS variables defined, import the default variables provided by the package: ```tsx theme={null} import '@yoopta/themes-shadcn/variables.css'; ``` This gives you a complete set of light and dark mode color tokens. ### Dark mode The theme supports dark mode through CSS variables. When dark mode is active, the variable values change and the editor automatically picks up the new colors (background, text, borders, etc.). Dark mode activates when any ancestor of the editor has one of these selectors: * `.dark` class (default for shadcn/ui and Tailwind dark mode) * `[data-theme="dark"]` attribute * `[data-yoopta-theme="dark"]` attribute **Example: toggle dark mode** ```tsx theme={null} function App() { const [isDark, setIsDark] = useState(false); return (
{/* ... */}
); } ``` Or using a data attribute: ```tsx theme={null}
``` ### Customizing variables You can override any variable to match your brand. The theme uses HSL values (without the `hsl()` wrapper): ```css theme={null} :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 221.2 83.2% 53.3%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... your dark mode overrides */ } ``` You only need to define the variables you want to change — the rest will use defaults from `variables.css` (if imported) or your existing shadcn/ui setup. **Full list of variables used by the theme:** | Variable | Usage | | -------------------------------------------- | --------------------------------------- | | `--background` | Editor background | | `--foreground` | Editor text color | | `--card` / `--card-foreground` | Card-style blocks (callout, code, etc.) | | `--popover` / `--popover-foreground` | Dropdowns, menus, tooltips | | `--primary` / `--primary-foreground` | Primary actions, buttons | | `--secondary` / `--secondary-foreground` | Secondary actions | | `--muted` / `--muted-foreground` | Muted text, disabled states | | `--accent` / `--accent-foreground` | Hover highlights, accents | | `--destructive` / `--destructive-foreground` | Delete actions, errors | | `--border` | Borders | | `--input` | Input field borders | | `--ring` | Focus rings | | `--radius` | Border radius base value | *** ## @yoopta/themes-material Material Design styled components for Yoopta plugins. **In progress** — not all plugins may be covered yet. **Install** ```bash theme={null} npm install @yoopta/themes-material ``` **Usage** (same idea as Shadcn: `applyTheme(plugins)` or `plugin.extend({ elements: MaterialUI })` per plugin when the package exposes it). *** ## Summary | Approach | Use case | | -------------------------------------- | ----------------------------------------------- | | Headless | Custom design; you implement all element UI. | | `plugin.extend({ elements: ThemeUI })` | Use theme UI only for specific plugins. | | `applyTheme(plugins)` | Use theme UI for all supported plugins at once. | The editor and plugins stay headless; themes only provide optional UI for their elements. # Introduction Source: https://docs.yoopta.dev/introduction Learn about Yoopta Editor — a headless, plugin-based rich-text editor for React ## Why Yoopta? I've integrated rich-text editors into products more times than I'd like to admit. Every time it was the same story: wrestling with complex APIs, fighting against opinionated UI that doesn't fit your product, hacking around limitations. Yoopta was built to end that cycle. The idea is simple: give developers a **headless core** when they need full control, but also ship **20+ ready-made plugins**, **pre-built UI components** (toolbars, slash menus, block actions), and **theme presets** (shadcn, Material) — so you can launch a polished editing experience without building everything from scratch. Whether you're building a simple blog editor, a Notion-like workspace, a CMS for landing pages, or even a full website builder — Yoopta gives you the primitives to get there. If Yoopta saves you time, consider [starring the repo](https://github.com/Darginec05/Yoopta-Editor) or [sponsoring the project](https://github.com/sponsors/Darginec05). It keeps the project alive. ## How it works Yoopta Editor is **headless** — the core package (`@yoopta/editor`) handles all content logic (blocks, elements, operations, history) but renders no UI. You get full control over appearance. Don't want to build everything yourself? Yoopta also ships: * **Themes** (`@yoopta/themes-shadcn`) — pre-styled components you can apply in one line with `applyTheme()` * **UI components** (`@yoopta/ui`) — toolbar, slash command menu, block actions, and more * **20+ plugins** — paragraph, headings, lists, tables, images, code, embeds, and more Each block in the editor runs its own [Slate.js](https://docs.slatejs.org/) instance, which means blocks are isolated, performant, and independently extensible. ## Examples & Demos See what you can build with Yoopta: Complete editor with toolbar, slash menu, block actions, drag & drop, and mentions Visual page builder with drag-and-drop blocks, sidebar settings, and live preview Microsoft Word-inspired editor with fixed toolbar, formatting, and export Email composition with templates, split-view editing, and email-safe export GitHub-flavored Markdown editor with live preview and download as README.md Slack-style messaging with channel list, rich text composer, and mentions WhatsApp/Instagram-like chat with message bubbles, attachments, and reactions Real-time multi-user editing with Yjs, remote cursors, and presence awareness All examples with source code are available in the [examples directory](https://github.com/Darginec05/Yoopta-Editor/tree/main/web/next-app-example/app/examples). ## Quick Example ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; import { Bold, Italic } from '@yoopta/marks'; import { applyTheme } from '@yoopta/themes-shadcn'; const plugins = applyTheme([Paragraph]); const marks = [Bold, Italic]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks }), []); return ( console.log(value)} placeholder="Type something..." /> ); } ``` ## Packages Install only what you need: | Category | Packages | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | **Core** | `@yoopta/editor` | Headless editor engine (requires `slate`, `slate-react`, `slate-dom`) | | **UI** | `@yoopta/ui` | Toolbar, ActionMenuList, SlashCommandMenu, BlockOptions, FloatingBlockActions | | **Marks** | `@yoopta/marks` | Bold, Italic, Underline, Strike, Code, Highlight | | **Text** | `@yoopta/paragraph`, `@yoopta/headings`, `@yoopta/blockquote` | Basic text blocks | | **Lists** | `@yoopta/lists` | Bulleted, numbered, and todo lists | | **Media** | `@yoopta/image`, `@yoopta/video`, `@yoopta/file`, `@yoopta/embed` | Media and embed blocks | | **Layout** | `@yoopta/table`, `@yoopta/accordion`, `@yoopta/tabs`, `@yoopta/steps`, `@yoopta/carousel`, `@yoopta/callout`, `@yoopta/divider` | Structured layout blocks | | **Inline** | `@yoopta/link`, `@yoopta/mention`, `@yoopta/emoji`, `@yoopta/math` | Inline elements | | **Code** | `@yoopta/code` | Code block with syntax highlighting | | **Math** | `@yoopta/math` | Code block with syntax highlighting | | **Themes** | `@yoopta/themes-shadcn`, `@yoopta/themes-material` | Pre-styled plugin components | | **Exports** | `@yoopta/exports` | HTML, Markdown, plain text, email serializers | | **Collaboration** | `@yoopta/collaboration` | Real-time collaboration via Yjs | ## Key Features Full control over UI. Use your own components, apply a theme, or mix both. 20+ built-in plugins. Create custom ones or extend existing plugins with `.extend()`. Inject plugins inside other plugins — lists inside callouts, code inside tabs, and more. Reorder blocks, nest content, and drag elements with built-in DnD support. Serialize to HTML, Markdown, plain text, or email-safe HTML out of the box. Build CMS dashboards, landing page builders, or full website builders — Yoopta provides the content primitives, you define the experience. Full type safety across the editor, plugins, and all APIs. ## Next Steps Install and build your first editor in 5 minutes Deep dive into editor architecture Complete API documentation ## Community & Support * **[Discord](https://discord.gg/Dt8rhSTjsn)** — Chat with the team and community * **[GitHub Discussions](https://github.com/Darginec05/Yoopta-Editor/discussions)** — Ask questions and share ideas * **[GitHub Issues](https://github.com/Darginec05/Yoopta-Editor/issues)** — Report bugs or request features * **[Twitter](https://twitter.com/LebovskiYoo)** — Follow for updates If Yoopta Editor is useful to you, please give it a star on [GitHub](https://github.com/Darginec05/Yoopta-Editor)! # Marks Overview Source: https://docs.yoopta.dev/marks/overview Text formatting (marks) in Yoopta Editor ## Introduction **Marks** in Yoopta Editor are text formatting options: bold, italic, underline, strike, code, highlight, and custom marks (e.g. color, font size). They are applied to the current selection or to specific blocks. Marks are registered when you create the editor and controlled via the `Marks` namespace. Marks are not on the editor instance—use the `Marks` namespace from `@yoopta/editor`. Register marks in `createYooptaEditor({ marks: [...] })`. ## Built-in marks The `@yoopta/marks` package provides ready-to-use marks: * **Bold** — `Cmd/Ctrl + B` * **Italic** — `Cmd/Ctrl + I` * **Underline** — `Cmd/Ctrl + U` * **Strike** — `Cmd/Ctrl + Shift + S` * **CodeMark** — `Cmd/Ctrl + E` * **Highlight** — Text highlighting with colors Install and pass them to `createYooptaEditor`: ```bash theme={null} yarn add @yoopta/marks ``` ```tsx theme={null} import { createYooptaEditor } from '@yoopta/editor'; import { Bold, Italic, Underline, Strike, CodeMark, Highlight } from '@yoopta/marks'; const marks = [Bold, Italic, Underline, Strike, CodeMark, Highlight]; const editor = createYooptaEditor({ plugins: PLUGINS, marks, }); ``` ## Using the Marks API Apply, toggle, or query marks with the `Marks` namespace: ```tsx theme={null} import { Marks } from '@yoopta/editor'; // Toggle bold on current selection Marks.toggle(editor, { type: 'bold' }); // Add color to selection Marks.add(editor, { type: 'color', value: '#ff0000' }); // Check if bold is active const isBold = Marks.isActive(editor, { type: 'bold' }); // Get current mark value const color = Marks.getValue(editor, { type: 'color' }); // Remove a mark Marks.remove(editor, { type: 'bold' }); // Clear all marks Marks.clear(editor); ``` You can target specific blocks with `at` or `blockId`, or a Slate `selection` range. See [Marks API](/api-reference/marks) for all methods and options. ## Custom marks Define your own marks with `createYooptaMark`: give them a `type`, optional `hotkey`, and a `render` function. Pass the result into `createYooptaEditor({ marks: [...] })`. ```tsx theme={null} import { createYooptaMark } from '@yoopta/editor'; const HighlightMark = createYooptaMark({ type: 'highlight', hotkey: 'mod+shift+h', render: (props) => ( {props.children} ), }); const editor = createYooptaEditor({ plugins: PLUGINS, marks: [Bold, Italic, HighlightMark], }); ``` ## Next steps Full API: update, add, remove, toggle, isActive, getValue, getAll, clear createYooptaEditor options and YooEditor instance # Accordion Source: https://docs.yoopta.dev/plugins/accordion Collapsible accordion sections ## Overview The Accordion plugin creates collapsible content sections. Each item has a heading (click to expand/collapse) and content area. It uses native `
` / `` in the default render and supports HTML serialization from those elements. ## Installation ```bash theme={null} npm install @yoopta/accordion ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Accordion from '@yoopta/accordion'; const plugins = [Accordion]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Expand/Collapse**: Each item can be expanded or collapsed via `isExpanded` prop * **Keyboard**: Enter toggles expansion when in heading, or inserts a new item; Backspace removes empty items or the block * **HTML**: Deserializes from `
` / `` / `

`; serializes to the same * **Email**: Serializes to table-based layout for email clients * **Shortcuts**: Type `accordion` to insert ## Structure The Accordion block consists of nested elements: ``` accordion-list └── accordion-list-item (multiple) ├── accordion-list-item-heading └── accordion-list-item-content ``` ## Configuration ```jsx theme={null} import Accordion from '@yoopta/accordion'; const plugins = [Accordion]; ``` ## Options Display title in UI menus Description shown in menus Keyboard shortcuts to trigger the plugin in slash menu ## Element props Whether the accordion item is expanded (open) or collapsed ## Commands Accordion commands are available via the plugin's `commands` and through the editor. Use the **`Blocks`** namespace from `@yoopta/editor` for generic block operations (insert, delete, update meta) and **Accordion commands** for accordion structure. ### insertAccordion Insert a new Accordion block. ```typescript theme={null} import Accordion, { AccordionCommands } from '@yoopta/accordion'; AccordionCommands.insertAccordion(editor, { items: 2, props: { isExpanded: false }, at: 0, focus: true, }); ``` Number of accordion items to create initially Initial expanded state for each item Index or path where to insert the block Whether to focus the new block after insert ### buildAccordionElements Build the accordion element structure (used internally by `insertAccordion`). ```typescript theme={null} import { AccordionCommands } from '@yoopta/accordion'; const accordionList = AccordionCommands.buildAccordionElements(editor, { items: 3, props: { isExpanded: true }, }); ``` ### deleteAccordion Remove an Accordion block by id. ```typescript theme={null} import { AccordionCommands } from '@yoopta/accordion'; AccordionCommands.deleteAccordion(editor, blockId); ``` ## Block operations You can also use the generic Blocks API: ```typescript theme={null} import { Blocks } from '@yoopta/editor'; // Insert (use AccordionCommands.insertAccordion for correct initial structure) Blocks.insertBlock(editor, 'Accordion', { blockData: { ... } }); // Delete Blocks.deleteBlock(editor, { blockId }); // Update block meta (e.g. align, depth) Blocks.updateBlock(editor, blockId, { meta: { align: 'center' } }); ``` ## Element operations To toggle an item's expanded state or add/remove items, use the Elements API: ```typescript theme={null} import { Elements } from '@yoopta/editor'; // Toggle expanded state of an accordion item Elements.updateElement(editor, { blockId, type: 'accordion-list-item', props: { isExpanded: !currentExpanded }, path: listItemPath, }); // Insert new accordion item Elements.insertElement(editor, { blockId, type: 'accordion-list-item', props: { isExpanded: true }, at: 'next', focus: true, }); // Delete accordion item Elements.deleteElement(editor, { blockId, type: 'accordion-list-item', path: listItemPath, }); ``` ## Keyboard behavior * **Enter** in heading: toggles the item's expanded state * **Enter** in content: inserts a new accordion item after the current one * **Backspace** in empty heading: removes the item (or deletes the block if it's the only item) * **Backspace** in empty content (when focus is in content): prevented so structure is preserved ## Theme (Shadcn) The `@yoopta/themes-shadcn` package provides styled accordion elements. Apply the theme so the Accordion block uses Shadcn UI: ```jsx theme={null} import { applyTheme } from '@yoopta/themes-shadcn'; import Accordion from '@yoopta/accordion'; const plugins = applyTheme([Accordion]); ``` The Shadcn accordion theme maps: * `accordion-list` → `AccordionList` * `accordion-list-item` → `AccordionListItem` (uses `element.props.isExpanded` for `data-state`) * `accordion-list-item-heading` → `AccordionItemHeading` * `accordion-list-item-content` → `AccordionItemContent` ## Parsers ### HTML * **Deserialize**: `

` with `` and `

` children * **Serialize**: Accordion block → `

` containing `
` / `` / `

`; block meta `align` and `depth` are stored in `data-meta-align` and `data-meta-depth` on each `

` ### Email * **Serialize**: Table-based layout for compatibility with email clients; headings and content in table rows with inline styles. ## Related * [Tabs](/plugins/tabs) — Tabbed content * [Steps](/plugins/steps) — Step-by-step instructions * [Blockquote](/plugins/blockquote) — Quote blocks # Blockquote Source: https://docs.yoopta.dev/plugins/blockquote Quote blocks ## Overview The Blockquote plugin provides styled quote blocks for highlighting citations, testimonials, or important excerpts. # Callout Source: https://docs.yoopta.dev/plugins/callout Highlighted callout blocks ## Overview The Callout plugin creates highlighted notice blocks for tips, warnings, notes, or other call-to-attention content. # Carousel Source: https://docs.yoopta.dev/plugins/carousel Display images or content in a carousel/slider format ## Overview The Carousel plugin allows you to create interactive carousels to display images or content in a slider format. It's perfect for showcasing multiple items in a compact, scrollable interface. ## Installation ```bash theme={null} npm install @yoopta/carousel ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Carousel from '@yoopta/carousel'; const plugins = [Carousel]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Multiple Items**: Display multiple carousel items * **Keyboard Shortcuts**: Type `carousel` to insert * **Responsive**: Adapts to different screen sizes * **Customizable**: Extend with custom rendering ## Structure The Carousel plugin consists of nested elements: ``` carousel-container └── carousel-list-item (multiple) ├── content └── ... ``` ## Commands ### buildCarouselElements Creates the initial carousel structure. ```typescript theme={null} CarouselCommands.buildCarouselElements(editor, { items: 5 }); ``` Number of carousel items to create (default: 5) ## Extending the Plugin ### Custom Rendering ```jsx theme={null} import { CarouselPlugin } from '@yoopta/carousel'; const CustomCarousel = CarouselPlugin.extend({ elements: { 'carousel-container': { render: (props) => (
{props.children}
), }, 'carousel-list-item': { render: (props) => (
{props.children}
), }, }, }); ``` ### Integration with Embla Carousel For advanced carousel functionality, you can integrate with [Embla Carousel](https://www.embla-carousel.com/): ```jsx theme={null} import { CarouselPlugin } from '@yoopta/carousel'; import useEmblaCarousel from 'embla-carousel-react'; const CustomCarousel = CarouselPlugin.extend({ elements: { 'carousel-container': { render: (props) => { const [emblaRef] = useEmblaCarousel({ loop: true }); return (
{props.children}
); }, }, }, }); ``` ## Options Display title in UI Description shown in menus Keyboard shortcuts to trigger the plugin ## Use Cases Display multiple images in a scrollable carousel Show product features or variations Rotate through customer testimonials Showcase portfolio items or projects ## Best Practices Ensure images are optimized for web to maintain carousel performance Keep carousel items reasonable (5-10) for better UX Provide clear navigation controls for users ## Related Plugins * [Image Plugin](/plugins/image) - For individual images * [Tabs Plugin](/plugins/tabs) - For tabbed content organization # Code Source: https://docs.yoopta.dev/plugins/code Syntax-highlighted code blocks with theme support ## Overview The Code plugin provides syntax-highlighted code blocks powered by [Shiki](https://shiki.style/). It supports multiple programming languages and themes, making it perfect for technical documentation and tutorials. ## Installation ```bash theme={null} npm install @yoopta/code ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Code from '@yoopta/code'; const plugins = [Code]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Syntax Highlighting**: Powered by Shiki with accurate highlighting * **200+ Languages**: Support for all major programming languages * **Multiple Themes**: Choose from various color themes * **Code Copying**: Easy code copying functionality * **Line Numbers**: Optional line number display * **Keyboard Shortcuts**: Type ` ``` `, `code`, or `js` to insert ## Configuration ```jsx theme={null} import { Code } from '@yoopta/code'; const plugins = [ Code.extend({ options: { theme: 'github-dark', language: 'typescript', }, }), ]; ``` ## Options Shiki theme for syntax highlighting. Popular options: - `github-dark` - `github-light` - `dracula` * `monokai` - `nord` - `tokyo-night` Default programming language. Supported languages include: - JavaScript, TypeScript - Python, Java, C++, Go, Rust - HTML, CSS, SCSS - JSON, YAML, XML - And 190+ more Display title in UI Description shown in menus Keyboard shortcuts to trigger the plugin ## Element Props Each code block has the following properties: Programming language for syntax highlighting Color theme for the code block ## Commands Use the **`Blocks`** namespace for block type changes and **`CodeCommands`** (or **`Elements.updateElement`**) for code element props: ```typescript theme={null} import { Blocks } from '@yoopta/editor'; import { CodeCommands } from '@yoopta/code'; // Transform current block to a Code block Blocks.toggleBlock(editor, 'Code', { preserveContent: true }); // Update language / theme (recommended — keeps Slate structure valid) CodeCommands.updateCodeLanguage(editor, blockId, 'python'); CodeCommands.updateCodeTheme(editor, blockId, 'dracula'); ``` ## Custom Rendering ```jsx theme={null} import { Code } from '@yoopta/code'; const CustomCode = Code.extend({ elements: { code: { render: (props) => { const { language, theme } = props.element.props; return (
{language}
              {props.children}
            
); }, }, }, }); ``` ## Keyboard Behavior The Code plugin has special keyboard handling: * **Enter**: Inserts a newline within the code block (not a new block) * **Shift+Enter**: Also inserts a newline * **Tab**: Inserts tab character (doesn't switch focus) ## Parsers ### HTML Serialization ```html theme={null}
  console.log("Hello World");
``` ### Markdown Serialization ````markdown theme={null} ```javascript console.log('Hello World'); ``` ```` ### Email Serialization The plugin provides email-compatible HTML with inline styles for proper rendering in email clients. ## Extending the Plugin ### Custom Theme ```jsx theme={null} const CustomCode = Code.extend({ elements: { code: { props: { theme: 'nord', language: 'typescript', }, }, }, }); ``` ### With Line Numbers ```jsx theme={null} const CustomCode = Code.extend({ elements: { code: { render: (props) => { const code = props.element.children[0]?.text || ''; const lines = code.split('\n'); return (
{lines.map((_, i) => (
{i + 1}
))}
{props.children}
); }, }, }, }); ``` ## Use Cases Technical documentation with code examples Step-by-step coding tutorials API endpoints and code samples Technical blog posts with code snippets ## Best Practices Select a theme that matches your site's design and provides good contrast Always specify the language for accurate syntax highlighting Break long code examples into smaller, focused snippets Include comments in code to explain complex logic ## Related Plugins * [CodeGroup Plugin](/plugins/code-group) - For tabbed code blocks * [Paragraph Plugin](/plugins/paragraph) - For text content # CodeGroup Source: https://docs.yoopta.dev/plugins/code-group Display multiple code files in tabbed interface ## Overview The CodeGroup plugin allows you to create tabbed code blocks, perfect for showing the same code in different languages or displaying multiple related files. Each tab can have its own language and syntax highlighting. ## Installation ```bash theme={null} npm install @yoopta/code ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import { CodeGroup } from '@yoopta/code'; const plugins = [CodeGroup]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Multiple Tabs**: Display code in multiple tabs * **Syntax Highlighting**: Powered by Shiki * **Per-Tab Languages**: Each tab can have different language * **Active Tab State**: Track which tab is active * **Theme Support**: Consistent theming across tabs ## Structure The CodeGroup plugin consists of nested elements: ``` code-group-container (props: activeTabId, theme) ├── code-group-list │ └── code-group-item-heading (multiple tabs) └── code-group-content (props: referenceId, language) ``` ## Configuration ```jsx theme={null} import { CodeGroup } from '@yoopta/code'; const plugins = [ CodeGroup.extend({ options: { theme: 'dracula', }, }), ]; ``` ## Options Shiki theme for all code blocks in the group Display title in UI Description shown in menus ## Element Props ### code-group-container ID of the currently active tab Theme for all code blocks ### code-group-content ID linking content to its tab heading Programming language for this code block ## Commands ### addTabItem Add a new tab to the code group. ```typescript theme={null} import { CodeGroupCommands } from '@yoopta/code'; CodeGroupCommands.addTabItem(editor, blockId, { at: selection }); ``` ## Keyboard Behavior * **Enter on Tab Heading**: Creates a new tab * **Enter in Code Content**: Inserts newline (not a new block) * **Backspace**: Prevents deletion at content start * **Cmd/Ctrl+A**: Selects entire code content ## Initial Structure When created, the plugin initializes with: ```typescript theme={null} { 'code-group-container': { activeTabId: 'generated-id', theme: 'github-dark', }, tabs: [ { heading: 'hello-world.ts', content: 'console.log("Hello World");', language: 'typescript', }, ], } ``` ## Custom Rendering ```jsx theme={null} import { CodeGroup } from '@yoopta/code'; const CustomCodeGroup = CodeGroup.extend({ elements: { 'code-group-container': { render: (props) => { const { activeTabId, theme } = props.element.props; return (
{props.children}
); }, }, 'code-group-list': { render: (props) => (
{props.children}
), }, 'code-group-item-heading': { render: (props) => { const isActive = props.element.id === getActiveTabId(); return ( ); }, }, 'code-group-content': { render: (props) => { const { language } = props.element.props; return (
            {props.children}
          
); }, }, }, }); ``` ## Use Cases Show same code in JavaScript, Python, Go, etc. Display multiple files that work together Show code before and after refactoring Different response formats (JSON, XML, etc.) ## Best Practices Use descriptive names for tabs (e.g., "package.json", "server.ts") Group related code in logical language combinations Keep number of tabs reasonable (2-5) for better UX Ensure code in different tabs is related or equivalent ## Related Plugins * [Code Plugin](/plugins/code) - For single code blocks * [Tabs Plugin](/plugins/tabs) - For general tabbed content # Divider Source: https://docs.yoopta.dev/plugins/divider Horizontal divider line ## Overview The Divider plugin inserts a horizontal line to separate content sections. # Embed Source: https://docs.yoopta.dev/plugins/embed Embed content from YouTube, Vimeo, Twitter, Spotify, Figma, and more ## Overview The Embed plugin provides a universal solution for embedding external content from various platforms. It supports video providers, social media, code sandboxes, music services, and maps — all with automatic URL detection and proper aspect ratio handling. ## Installation ```bash theme={null} npm install @yoopta/embed ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Embed from '@yoopta/embed'; const plugins = [Embed]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Universal Embedding**: Embed content from 13+ platforms with a single plugin * **Auto URL Detection**: Automatically detects provider from pasted URLs * **Provider-specific Aspect Ratios**: Each provider uses its optimal aspect ratio * **Resizable**: Resize embeds while maintaining aspect ratio * **oEmbed Support**: Fetches metadata from providers that support oEmbed * **Responsive**: Automatically adapts to container width ## Supported Providers Videos, Shorts Videos Tweets, Posts Posts, Reels Tracks, Albums, Playlists Tracks, Sets Files, Prototypes Pens Sandboxes Videos Videos Videos Maps, Places ## Configuration ```jsx theme={null} import Embed from '@yoopta/embed'; const plugins = [ Embed.extend({ options: { maxWidth: 800, // Maximum width in pixels defaultSizes: { width: 650, height: 400, }, }, }), ]; ``` ## Options Maximum width for embedded content in pixels Default dimensions for embeds when aspect ratio cannot be determined Default width in pixels Default height in pixels ## Element Props Provider information for the embedded content: ```typescript theme={null} { type: EmbedProviderType; // 'youtube' | 'vimeo' | 'twitter' | etc. id: string; // Provider-specific content ID url: string; // Original URL embedUrl: string; // URL used in iframe meta?: { title?: string; description?: string; thumbnailUrl?: string; authorName?: string; authorUrl?: string; }; } ``` Embed dimensions: `{ width: number, height: number }` ## Provider Types ```typescript theme={null} type EmbedProviderType = | 'youtube' | 'vimeo' | 'dailymotion' | 'wistia' | 'loom' | 'twitter' | 'instagram' | 'figma' | 'codepen' | 'codesandbox' | 'spotify' | 'soundcloud' | 'google-maps' | 'unknown'; ``` ## Commands ```typescript theme={null} import { EmbedCommands } from '@yoopta/embed'; // Insert an embed from URL EmbedCommands.insertEmbed(editor, { url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', }); // Check if URL is supported const isSupported = EmbedCommands.isEmbedUrl('https://vimeo.com/123456789'); ``` ## Utility Functions The Embed plugin exports utility functions for working with embed URLs: ```typescript theme={null} import { parseEmbedUrl, isEmbedUrl, detectProvider, getProviderConfig, getProviderAspectRatio, calculateEmbedDimensions, getSupportedProviders, PROVIDER_CONFIGS, } from '@yoopta/embed'; // Parse a URL and get provider info const provider = parseEmbedUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); // { // type: 'youtube', // id: 'dQw4w9WgXcQ', // url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', // embedUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ' // } // Check if URL is embeddable const canEmbed = isEmbedUrl('https://open.spotify.com/track/...'); // true // Detect provider type const providerType = detectProvider('https://codepen.io/user/pen/abc123'); // 'codepen' // Get aspect ratio for provider const ratio = getProviderAspectRatio('youtube'); // { width: 16, height: 9 } // Calculate dimensions based on max width const dimensions = calculateEmbedDimensions('youtube', 800); // { width: 800, height: 450 } // Get list of supported providers const providers = getSupportedProviders(); // [{ type: 'youtube', name: 'YouTube' }, ...] ``` ## URL Patterns Each provider supports specific URL patterns: ### YouTube ``` youtube.com/watch?v=VIDEO_ID youtu.be/VIDEO_ID youtube.com/embed/VIDEO_ID youtube.com/shorts/VIDEO_ID ``` ### Vimeo ``` vimeo.com/VIDEO_ID player.vimeo.com/video/VIDEO_ID ``` ### Twitter/X ``` twitter.com/user/status/TWEET_ID x.com/user/status/TWEET_ID ``` ### Instagram ``` instagram.com/p/POST_ID instagram.com/reel/REEL_ID instagram.com/tv/TV_ID ``` ### Spotify ``` open.spotify.com/track/TRACK_ID open.spotify.com/album/ALBUM_ID open.spotify.com/playlist/PLAYLIST_ID open.spotify.com/episode/EPISODE_ID open.spotify.com/show/SHOW_ID ``` ### Figma ``` figma.com/file/FILE_ID figma.com/proto/FILE_ID figma.com/design/FILE_ID ``` ### CodePen ``` codepen.io/USER/pen/PEN_ID codepen.io/USER/full/PEN_ID ``` ### CodeSandbox ``` codesandbox.io/s/SANDBOX_ID codesandbox.io/embed/SANDBOX_ID ``` ## Hooks ### useEmbedUrl Parse URLs and fetch oEmbed data: ```typescript theme={null} import { useEmbedUrl } from '@yoopta/embed'; const MyComponent = () => { const { provider, oEmbedData, loading, error, embedProps } = useEmbedUrl( 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' ); if (loading) return
Loading...
; if (error) return
Error: {error}
; return (

Provider: {provider?.type}

Title: {oEmbedData?.title}

Dimensions: {embedProps.sizes?.width} x {embedProps.sizes?.height}

); }; ``` ## Custom Rendering ```jsx theme={null} import Embed from '@yoopta/embed'; const CustomEmbed = Embed.extend({ elements: { embed: { render: (props) => { const { provider, sizes } = props.element.props; if (!provider) { return (
Enter a URL to embed content
{props.children}
); } return (
``` It also handles divs with the `data-yoopta-embed` attribute: ```html theme={null}
``` ### HTML Serialization ```html theme={null}
``` ### Markdown Serialization Embeds are serialized as links in Markdown: ```markdown theme={null} [YouTube](https://www.youtube.com/watch?v=dQw4w9WgXcQ) ``` ### Email Serialization For email clients that don't support iframes, embeds are rendered as clickable thumbnails (when available) or placeholder links: ```html theme={null} Video title ``` ## Aspect Ratios Each provider has its optimal aspect ratio: | Provider | Aspect Ratio | | ----------- | ------------ | | YouTube | 16:9 | | Vimeo | 16:9 | | Dailymotion | 16:9 | | Loom | 16:9 | | Wistia | 16:9 | | Figma | 16:9 | | CodePen | 16:9 | | CodeSandbox | 16:9 | | Google Maps | 16:9 | | Twitter | 550:600 | | Instagram | 1:1 | | Spotify | 300:380 | | SoundCloud | 100:166 | ## Use Cases Embed YouTube tutorials, Spotify playlists, or tweets Include Figma designs, CodePen demos, or Loom walkthroughs Showcase Vimeo videos, Dribbble shots, or CodeSandbox projects Embed Instagram posts, Twitter threads, or TikTok videos Include Spotify tracks, SoundCloud sets, or podcast episodes Share CodePen experiments, CodeSandbox demos, or GitHub gists ## Best Practices Choose the right provider for your content type. Use Video plugin for self-hosted videos, Embed plugin for third-party content. Embeds load external content which can affect page performance. Consider lazy loading for content-heavy pages. Some embeds may not load due to privacy settings or content restrictions. Consider providing fallback content or links. Some platforms have embedding restrictions. Check provider policies for commercial use. Configure appropriate maxWidth for your design to maintain consistency across embed types. ## Troubleshooting If a URL is not being detected, check: 1. The URL format matches supported patterns 2. The URL is from a supported provider **Solution:** Use the `parseEmbedUrl` utility to debug: ```typescript theme={null} import { parseEmbedUrl } from '@yoopta/embed'; const provider = parseEmbedUrl('your-url-here'); console.log(provider); // Check if provider is not null ``` Some content may be blocked due to: 1. Content is private or restricted 2. Platform blocks embedding from your domain 3. CORS restrictions **Solution:** Check the provider's embedding policies and ensure content is publicly embeddable. If the aspect ratio looks wrong: 1. The provider may have changed their default dimensions 2. The content may have a non-standard aspect ratio **Solution:** Resize the embed manually using the resize handles. Not all providers support oEmbed. The plugin will still work but may not have: 1. Thumbnail images 2. Title/description metadata 3. Author information **Solution:** This is expected behavior. The embed will still function correctly. ## Embed vs Video Plugin | Feature | Embed Plugin | Video Plugin | | ------------------ | ------------ | ------------ | | Self-hosted videos | ❌ | ✅ | | YouTube/Vimeo | ✅ | ✅ | | File upload | ❌ | ✅ | | Social media | ✅ | ❌ | | Code sandboxes | ✅ | ❌ | | Music services | ✅ | ❌ | | Maps | ✅ | ❌ | | Playback settings | ❌ | ✅ | | Poster images | ❌ | ✅ | **Recommendation:** Use the Video plugin for self-hosted videos or when you need playback controls. Use the Embed plugin for third-party content from social media, code sandboxes, music services, and maps. ## Related Plugins * [Video Plugin](/plugins/video) - For self-hosted videos and video providers with playback controls * [Image Plugin](/plugins/image) - For image content * [File Plugin](/plugins/file) - For file attachments # Emoji Source: https://docs.yoopta.dev/plugins/emoji Insert emoji by typing shortcodes with autocomplete dropdown ## Overview The Emoji plugin lets users insert emoji by typing a trigger character (default: `:`) followed by a search query. A dropdown with matching emoji appears, and selecting one inserts the Unicode character directly into the text — no special inline nodes, just plain text. ## Installation ```bash theme={null} npm install @yoopta/emoji ``` ## Basic Usage Pass the plugin to `createYooptaEditor` (wrapped with `withEmoji`); do not pass `plugins` to ``. Add `EmojiDropdown` as a child when using theme UI. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Emoji, { withEmoji } from '@yoopta/emoji'; import { EmojiDropdown } from '@yoopta/themes-shadcn/emoji'; const plugins = [Emoji]; export default function Editor() { const editor = useMemo( () => withEmoji(createYooptaEditor({ plugins, marks: [] })), [], ); return ( {}}> ); } ``` **Works out of the box** The Emoji plugin ships with a built-in dataset of \~400 common emoji. No configuration is needed for basic usage — just add the plugin and it works. You can provide a custom `onSearch` function to use your own dataset or API. See the [Configuration](#configuration) section below. ## Features * **Works Out of the Box**: Ships with \~400 built-in emoji — no configuration required * **Shortcode Trigger**: Type `:` to open the emoji picker, then search by name * **Plain Text Insertion**: Inserts Unicode emoji characters — no special nodes in the document * **Autocomplete Dropdown**: Searchable dropdown with keyboard navigation * **Custom Search**: Optionally provide your own search logic (local data, API, emoji-mart, etc.) * **Keyboard Navigation**: Arrow keys, Enter, Escape support * **Debounced Search**: Configurable debounce delay (default: 100ms) * **Multiple Triggers**: Support multiple trigger characters if needed ## Configuration ### Zero Config (Built-in Dataset) The simplest setup — uses the built-in emoji dataset with \~400 common emoji: ```jsx theme={null} import Emoji from '@yoopta/emoji'; const plugins = [Emoji]; ``` ### Custom Search Function Provide your own `onSearch` to use a custom dataset or API: ```jsx theme={null} import Emoji from '@yoopta/emoji'; const plugins = [ Emoji.extend({ options: { onSearch: async (query) => { // Local emoji data or API call return emojiData.filter((e) => e.name.includes(query) || e.keywords?.some((k) => k.includes(query)), ); }, }, }), ]; ``` ### With emoji-mart Data ```jsx theme={null} import data from '@emoji-mart/data'; // Pre-process emoji-mart data into flat array const allEmoji = Object.values(data.emojis).map((e) => ({ emoji: e.skins[0].native, name: e.id, keywords: e.keywords || [], category: e.category, })); const plugins = [ Emoji.extend({ options: { onSearch: (query) => { const q = query.toLowerCase(); return allEmoji.filter( (e) => e.name.includes(q) || e.keywords.some((k) => k.includes(q)), ); }, }, }), ]; ``` ### Advanced Configuration ```jsx theme={null} const plugins = [ Emoji.extend({ options: { triggers: [{ char: ':', allowSpaces: false }], onSearch: async (query, trigger) => { return await fetchEmoji(query); }, debounceMs: 100, minQueryLength: 1, closeOnSelect: true, closeOnClickOutside: true, closeOnEscape: true, onSelect: (item, trigger) => { console.log('Inserted emoji:', item.emoji, item.name); }, onOpen: (trigger) => { console.log('Emoji dropdown opened'); }, onClose: () => { console.log('Emoji dropdown closed'); }, }, }), ]; ``` ## Options Array of trigger configurations. Each trigger defines a character(s) that opens the emoji dropdown. Character(s) that trigger the emoji dropdown (default: `':'`) Allow spaces in search query (default: false) Pattern that must precede the trigger (default: whitespace or start of line) Simple single trigger character (shorthand for `triggers: [{char}]`). Use this for a single trigger, or use `triggers` array for multiple triggers. Custom search function called when user types after trigger. If not provided, uses the built-in dataset (\~400 common emoji). **Signature:** ```typescript theme={null} (query: string, trigger: EmojiTrigger) => Promise | EmojiItem[]; ``` **Parameters:** * `query` - The search query (without trigger char) * `trigger` - The trigger that opened the dropdown **Returns:** Promise or array of `EmojiItem` objects Debounce delay for search in milliseconds. Since emoji search is often local, the default is lower than mention (100ms vs 300ms). Minimum query length before triggering search. Defaults to `1` since emoji shortcodes are meaningless at zero length. Called when an emoji is selected. **Signature:** ```typescript theme={null} (item: EmojiItem, trigger: EmojiTrigger) => void ``` Called when dropdown opens. **Signature:** ```typescript theme={null} (trigger: EmojiTrigger) => void ``` Called when dropdown closes. **Signature:** ```typescript theme={null} () => void ``` Close dropdown when item is selected. Close dropdown on click outside. Close dropdown on Escape key. ## EmojiItem Type Each item returned from `onSearch` should match: The Unicode emoji character (e.g., `'😀'`, `'🎉'`) Short name without colons (e.g., `'grinning'`, `'tada'`) Additional search keywords (e.g., `['happy', 'face']`) Category name (e.g., `'Smileys & Emotion'`, `'Objects'`) ## Commands ```typescript theme={null} import { EmojiCommands } from '@yoopta/emoji'; // Insert an emoji (replaces trigger + query with Unicode character) EmojiCommands.insertEmoji(editor, { emoji: '😀', name: 'grinning', }); // Dropdown control EmojiCommands.openDropdown(editor, { trigger: { char: ':' }, targetRect: { domRect, clientRects }, triggerRange: { blockId, path, startOffset }, }); EmojiCommands.closeDropdown(editor, 'manual'); // State const state = EmojiCommands.getState(editor); const query = EmojiCommands.getQuery(editor); EmojiCommands.setQuery(editor, 'smi'); const trigger = EmojiCommands.getTrigger(editor); // Utilities const triggers = EmojiCommands.getTriggers(editor); const colonTrigger = EmojiCommands.getTriggerByChar(editor, ':'); ``` ## Using with Themes When using a theme (e.g., `@yoopta/themes-shadcn`), you need to: 1. **Apply the theme** to your plugins 2. **Add the EmojiDropdown component** to your editor ```jsx theme={null} import Emoji, { withEmoji } from '@yoopta/emoji'; import { withShadcnUI } from '@yoopta/themes-shadcn'; import { EmojiDropdown } from '@yoopta/themes-shadcn/emoji'; const plugins = withShadcnUI([ Emoji, // Works with built-in dataset, or use Emoji.extend({ options: { onSearch } }) // ... other plugins ]); const editor = withEmoji( createYooptaEditor({ plugins, // ... }), ); {/* Add this component */} {/* ... other UI components */} ; ``` ## Custom Dropdown You can create a custom dropdown using the `useEmojiDropdown` hook: ```jsx theme={null} import { useEmojiDropdown } from '@yoopta/emoji'; function CustomEmojiDropdown() { const { isOpen, query, trigger, items, loading, error, selectedIndex, selectItem, refs, floatingStyles, } = useEmojiDropdown(); if (!isOpen) return null; return (
{loading &&
Searching...
} {error &&
Error: {error.message}
} {items.map((item, index) => (
selectItem(item)}> {item.emoji} :{item.name}:
))}
); } ``` ## Hooks ### useEmojiDropdown Hook for building custom emoji dropdowns: ```typescript theme={null} import { useEmojiDropdown } from '@yoopta/emoji'; const { // State isOpen, query, trigger, // Results items, loading, error, // Navigation selectedIndex, setSelectedIndex, // Actions selectItem, close, // Refs for floating-ui refs, floatingStyles, } = useEmojiDropdown({ debounceMs: 100, // Optional override }); ``` ### useEmojiState Read-only hook for tracking emoji picker state: ```typescript theme={null} import { useEmojiState } from '@yoopta/emoji'; const { isTypingEmoji, // boolean — whether user is currently typing a shortcode currentTrigger, // EmojiTrigger | null query, // string — current search query targetRect, // EmojiTargetRect | null — position for custom UI } = useEmojiState(); ``` ### useEmojiTriggerActive Check if a specific trigger is currently active: ```typescript theme={null} import { useEmojiTriggerActive } from '@yoopta/emoji'; const isColonActive = useEmojiTriggerActive(':'); ``` ## How It Works Unlike the Mention plugin which inserts inline void elements, the Emoji plugin inserts **plain Unicode text**. When a user types `:smile` and selects an emoji: 1. The trigger `:` and query `smile` are deleted from the document 2. The Unicode character `😊` is inserted as plain text via `Transforms.insertText` 3. No special nodes or elements are created — the emoji is just a text character This means emoji work naturally with all text operations (copy, paste, search, export) without needing special serialization. ## Use Cases Quick emoji insertion in chat-style editors Add expression to documents and notes Emoji in comment systems and feedback forms Consistent emoji support across team documents ## Best Practices The plugin ships with \~400 common emoji that work with zero config. For a more comprehensive dataset, use libraries like `@emoji-mart/data` and provide a custom `onSearch`. Emoji search is fast enough to run locally — avoid API calls when possible. Since emoji search is usually local, the default 100ms debounce is appropriate. Only increase it if you're making API calls. Return items with `keywords` arrays so users can find emoji by alternate names (e.g., searching "happy" finds 😀 "grinning"). Return a reasonable number of results (e.g., 10-20) to keep the dropdown manageable. Users typically pick from the first few results. The theme dropdowns show helpful messages for empty states. If building a custom dropdown, show "Type to search..." when the query is empty and "No emoji found" when there are no matches. ## Troubleshooting Make sure you've: 1. Wrapped your editor with `withEmoji()` 2. Added the `EmojiDropdown` component to your editor (when using themes) **Solution:** ```jsx theme={null} const editor = withEmoji(createYooptaEditor({ ... })); ``` If you're using a custom `onSearch` function, check that it: 1. Returns a Promise or array 2. Returns objects with `emoji` and `name` properties 3. Actually matches the query If you're not using a custom `onSearch`, the built-in dataset works automatically. **Solution:** ```jsx theme={null} onSearch: async (query) => { const results = await searchEmoji(query); return results.map((item) => ({ emoji: item.emoji, // Required — Unicode character name: item.name, // Required — shortcode name keywords: item.keywords, // Optional })); }, ``` The trigger only activates after whitespace or at the start of a line. If you're typing `:` immediately after a word (e.g., `hello:`), it won't trigger. This is intentional to avoid false positives with URLs and time formats. **Solution:** Type a space before `:` — e.g., `hello :smile`. Ensure the `insertEmoji` command has access to the trigger range. If you're calling it manually, the dropdown must have been opened first (which sets the trigger range). **Solution:** Use the dropdown UI or `selectItem` from the `useEmojiDropdown` hook, which handles insertion automatically. ## Related Plugins * [Mention Plugin](/plugins/mention) - Similar autocomplete pattern for mentioning users/resources * [Paragraph Plugin](/plugins/paragraph) - Text blocks that support emoji insertion # File Source: https://docs.yoopta.dev/plugins/file Upload and attach files with download support and file type icons ## Overview The File plugin provides a complete solution for uploading and attaching files to your content. It supports file uploads, displays file information (name, size, type), and provides download functionality with automatic file type detection and icons. ## Installation ```bash theme={null} npm install @yoopta/file ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import File from '@yoopta/file'; const plugins = [File]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` **Required Configuration** You **must** configure the `upload` option. Without this option, you'll see an error when trying to use the File plugin. ```jsx theme={null} const plugins = [ File.extend({ options: { upload: async (file) => { // Your upload logic here return { src: '...', name: file.name, size: file.size }; }, // delete is optional }, }), ]; ``` See the [Configuration](#configuration) section below for detailed examples. ## Features * **File Upload**: Upload files from device * **Custom Upload Functions**: Direct uploads to S3, Firebase, Cloudinary, etc. * **File Type Detection**: Automatic detection of file types (PDF, Document, Spreadsheet, etc.) * **File Icons**: Different icons for different file types * **File Size Display**: Human-readable file size formatting * **Download Support**: One-click download functionality * **File Deletion**: Optional deletion handling with custom functions * **Accept Filter**: Restrict allowed file types ## Configuration The File plugin supports two approaches for upload and delete operations: 1. **Endpoint-based**: Configure an API endpoint and the plugin handles the request 2. **Custom function**: Provide your own async function for complete control ### Endpoint-based Upload (Backend API) ```jsx theme={null} import File from '@yoopta/file'; const plugins = [ File.extend({ options: { upload: { endpoint: '/api/upload-file', method: 'POST', headers: { Authorization: `Bearer ${token}`, }, onSuccess: (result) => { console.log('Upload successful:', result); }, onError: (error) => { console.error('Upload failed:', error); }, }, // delete is optional delete: { endpoint: '/api/delete-file', method: 'DELETE', }, accept: '.pdf,.doc,.docx,.xls,.xlsx', maxFileSize: 10 * 1024 * 1024, // 10MB }, }), ]; ``` ### Custom Upload Function (Direct to Cloud) For direct uploads to third-party services like AWS S3, Firebase Storage, or Cloudinary: ```jsx theme={null} import File from '@yoopta/file'; const plugins = [ File.extend({ options: { // Custom upload function (required) upload: async (file, onProgress) => { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload', { method: 'POST', body: formData, }); const data = await response.json(); // Return FileUploadResponse return { id: data.id, src: data.url, name: file.name, size: file.size, format: file.name.split('.').pop(), }; }, // Custom delete function (optional) delete: async (element) => { await fetch(`/api/delete/${element.props.id}`, { method: 'DELETE' }); }, accept: '.pdf,.doc,.docx', }, }), ]; ``` The `delete` option is **optional**. If not provided, deleting a file block will only remove it from the editor without calling any storage deletion logic. ## Options ### upload (required) Can be either an **object** (endpoint-based) or a **function** (custom upload): URL endpoint for file upload HTTP method for upload Custom headers for upload request Form field name for the file Maximum file size in bytes Accepted file types (e.g., ".pdf,.doc,.docx") Callback for upload progress Callback when upload succeeds Callback when upload fails Custom async function for direct uploads to third-party services. **Signature:** ```typescript theme={null} (file: File, onProgress?: (progress: FileUploadProgress) => void) => Promise ``` **Returns `FileUploadResponse`:** * `id` - Unique identifier for the file (optional) * `src` - URL of the uploaded file (required) * `name` - File name (optional, defaults to original name) * `size` - File size in bytes (optional) * `format` - File extension (optional) ### delete (optional) Can be either an **object** (endpoint-based) or a **function** (custom delete): URL endpoint for file deletion HTTP method for deletion Custom headers for delete request Custom async function for deleting files from third-party services. **Signature:** ```typescript theme={null} (element: FileElement) => Promise ``` **Parameters:** * `element` - The file element containing props (id, src, name, etc.) Accepted file types. Can be file extensions (e.g., ".pdf,.doc") or MIME types (e.g., "application/pdf"). Maximum file size in bytes Global error handler for upload/delete operations ## Element Props Unique identifier for the file (from your storage) URL of the uploaded file File name (without extension) File size in bytes File extension (e.g., "pdf", "docx") ## Commands ```typescript theme={null} import { FileCommands } from '@yoopta/file'; // Insert a file FileCommands.insertFile(editor, { props: { src: 'https://example.com/document.pdf', name: 'document', format: 'pdf', size: 1024000, }, }); // Update file properties FileCommands.updateFile(editor, blockId, { name: 'renamed-document', }); // Delete file FileCommands.deleteFile(editor, blockId); ``` ## Utility Functions ### File Type Detection ```typescript theme={null} import { getFileType, getFileTypeLabel, getFileExtension, isFileType } from '@yoopta/file'; // Get file type from filename const fileType = getFileType('document.pdf'); // 'pdf' // Get human-readable label const label = getFileTypeLabel('spreadsheet.xlsx'); // 'Spreadsheet' // Get file extension const ext = getFileExtension('report.docx'); // 'docx' // Check if file is a specific type const isPdf = isFileType('pdf', 'document.pdf'); // true ``` ### File Size Formatting ```typescript theme={null} import { formatFileSize, parseFileSize } from '@yoopta/file'; // Format bytes to human-readable string formatFileSize(1024); // '1 KB' formatFileSize(1536000); // '1.46 MB' formatFileSize(1073741824); // '1 GB' // Parse size string back to bytes parseFileSize('1.5 MB'); // 1572864 ``` ## Supported File Types The plugin automatically detects and displays appropriate icons for these file types: | Type | Extensions | Icon | | ------------ | --------------------------------------------- | ---- | | PDF | `.pdf` | 📄 | | Document | `.doc`, `.docx`, `.odt`, `.rtf`, `.pages` | 📝 | | Spreadsheet | `.xls`, `.xlsx`, `.csv`, `.ods`, `.numbers` | 📊 | | Presentation | `.ppt`, `.pptx`, `.odp`, `.key` | 📽️ | | Image | `.jpg`, `.png`, `.gif`, `.webp`, `.svg` | 🖼️ | | Video | `.mp4`, `.webm`, `.mov`, `.avi` | 🎬 | | Audio | `.mp3`, `.wav`, `.ogg`, `.flac` | 🎵 | | Archive | `.zip`, `.rar`, `.7z`, `.tar`, `.gz` | 📦 | | Code | `.js`, `.ts`, `.py`, `.html`, `.css`, `.json` | 💻 | | Text | `.txt`, `.md`, `.log` | 📃 | ## Custom Rendering ```jsx theme={null} import File from '@yoopta/file'; import { getFileType, formatFileSize } from '@yoopta/file'; const CustomFile = File.extend({ elements: { file: { render: (props) => { const { src, name, format, size } = props.element.props; const displayName = format ? `${name}.${format}` : name; const fileType = getFileType(displayName); return ( ); }, }, }, }); ``` ## Shadcn Theme Integration When using `@yoopta/themes-shadcn`, the File plugin gets a beautiful UI automatically: ```jsx theme={null} import File from '@yoopta/file'; import applyTheme from '@yoopta/themes-shadcn'; const plugins = applyTheme([ File.extend({ options: { upload: async (file) => { // Your upload logic return { src: '...', name: file.name, size: file.size }; }, }, }), ]); ``` The Shadcn theme provides: * Upload placeholder with file picker * File card with type-specific icons and colors * Inline toolbar with actions (download, copy URL, replace, delete) * Upload progress indicator * Hover states and animations ## Parsers ### HTML Deserialization The plugin automatically deserializes `` tags with `download` attribute: ```html theme={null} document.pdf (1 MB) ``` ### HTML Serialization ```html theme={null} ``` ### Markdown Serialization ```markdown theme={null} [document.pdf](https://example.com/document.pdf) ``` ## Hooks ### useFileUpload ```typescript theme={null} import { useFileUpload } from '@yoopta/file'; // With custom function const { upload, loading, progress, error } = useFileUpload( async (file, onProgress) => { const response = await uploadToStorage(file); return { src: response.url, name: file.name, size: file.size }; } ); // With endpoint const { upload, loading, progress, error } = useFileUpload({ endpoint: '/api/upload-file', }); const handleUpload = async (file: File) => { const result = await upload(file); console.log('Uploaded:', result.url); }; ``` ### useFileDelete ```typescript theme={null} import { useFileDelete } from '@yoopta/file'; // With custom function (optional) const { deleteFile, loading, error } = useFileDelete( async (element) => { await deleteFromStorage(element.props.id); } ); // Without delete function (no-op) const { deleteFile } = useFileDelete(undefined); // deleteFile will just return success without calling storage ``` ## Use Cases Attach PDFs, Word docs, and spreadsheets Provide downloadable resources Build document libraries and archives Manage project assets and files ## Best Practices Configure `maxFileSize` to prevent oversized uploads: ```jsx theme={null} File.extend({ options: { upload: async (file) => { ... }, maxFileSize: 10 * 1024 * 1024, // 10MB }, }) ``` Use `accept` to restrict file types: ```jsx theme={null} File.extend({ options: { upload: async (file) => { ... }, accept: '.pdf,.doc,.docx,.xls,.xlsx', }, }) ``` Provide user feedback on errors: ```jsx theme={null} File.extend({ options: { upload: async (file) => { ... }, onError: (error) => { toast.error(`Upload failed: ${error.message}`); }, }, }) ``` Always return complete metadata from upload: ```jsx theme={null} return { id: data.id, // For deletion src: data.url, // For download name: file.name, // For display size: file.size, // For display format: extension, // For icon }; ``` ## Troubleshooting This error occurs when you haven't configured the `upload` option. **Solution:** Add an `upload` configuration to your File plugin: ```jsx theme={null} File.extend({ options: { upload: async (file) => { return { src: '...', name: file.name, size: file.size }; }, }, }) ``` This error occurs when using endpoint-based configuration without providing the `endpoint` URL. **Solution:** Make sure to include the `endpoint` property: ```jsx theme={null} File.extend({ options: { upload: { endpoint: '/api/upload-file', // Required! }, }, }) ``` Ensure the filename includes the correct extension. The plugin uses file extension for type detection. **Solution:** Return the format in your upload response: ```jsx theme={null} return { src: data.url, name: 'document', format: 'pdf', // Explicitly set the format size: file.size, }; ``` Make sure the `src` URL is accessible and has proper CORS headers for download. **Solution:** Ensure your storage returns files with `Content-Disposition: attachment` header or use the `download` attribute on links. ## Related Plugins * [Image Plugin](/plugins/image) - For image content * [Video Plugin](/plugins/video) - For video content * [Embed Plugin](/plugins/embed) - For embedded content # Headings Source: https://docs.yoopta.dev/plugins/headings H1, H2, H3 heading blocks ## Overview The Headings plugin provides H1, H2, and H3 block types for document structure and hierarchy. # Image Source: https://docs.yoopta.dev/plugins/image Upload and display images with full customization ## Overview The Image plugin provides a complete solution for adding images to your content. It supports file uploads, URL insertion, image resizing, and various display options. ## Installation ```bash theme={null} npm install @yoopta/image ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Image from '@yoopta/image'; const plugins = [Image]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` **Required Configuration** You **must** configure the `upload` option. Without this option, you'll see an error when trying to use the Image plugin. ```jsx theme={null} // Required when using themes const plugins = [ Image.extend({ options: { upload: async (file) => { // Your upload logic here return { id: '...', src: '...' }; }, // delete is optional }, }), ]; ``` See the [Configuration](#configuration) section below for detailed examples. ## Features * **File Upload**: Upload images from device * **URL Insertion**: Insert images via URL * **Custom Upload Functions**: Direct uploads to Cloudinary, S3, Firebase, etc. * **Image Resizing**: Resize images while maintaining aspect ratio * **Size Limits**: Set maximum width and height * **Object Fit**: Control how images fit their container * **Image Deletion**: Optional deletion handling with custom functions (if not configured, deletion only removes from editor) * **Responsive**: Automatically adapts to screen size ## Configuration The Image plugin supports two approaches for upload and delete operations: 1. **Endpoint-based**: Configure an API endpoint and the plugin handles the request 2. **Custom function**: Provide your own async function for complete control (useful for direct uploads to Cloudinary, S3, Firebase, etc.) ### Endpoint-based Upload (Backend API) ```jsx theme={null} import { Image } from '@yoopta/image'; const plugins = [ Image.extend({ options: { upload: { endpoint: '/api/upload', method: 'POST', headers: { Authorization: `Bearer ${token}`, }, onSuccess: (result) => { console.log('Upload successful:', result); }, onError: (error) => { console.error('Upload failed:', error); }, }, delete: { endpoint: '/api/delete', method: 'DELETE', }, maxSizes: { maxWidth: 800, maxHeight: 600, }, }, }), ]; ``` ### Custom Upload Function (Direct to Cloud) For direct uploads to third-party services like Cloudinary, AWS S3, or Firebase Storage, you can provide a custom async function: ```jsx theme={null} import { Image } from '@yoopta/image'; const plugins = [ Image.extend({ options: { // Custom upload function upload: async (file, onProgress) => { const formData = new FormData(); formData.append('file', file); formData.append('upload_preset', 'your_preset'); const response = await fetch( 'https://api.cloudinary.com/v1_1/your_cloud/image/upload', { method: 'POST', body: formData } ); const data = await response.json(); // Return ImageElementProps return { id: data.public_id, src: data.secure_url, sizes: { width: data.width, height: data.height, }, }; }, // Custom delete function delete: async (src) => { // Extract public_id and delete from your service const publicId = extractPublicId(src); await fetch(`/api/cloudinary-delete/${publicId}`, { method: 'DELETE' }); }, maxSizes: { maxWidth: 800, maxHeight: 600, }, }, }), ]; ``` ### Custom Fetch to Your API You can also use a custom function to call your own API with fetch: ```jsx theme={null} import { Image } from '@yoopta/image'; const plugins = [ Image.extend({ options: { upload: async (file, onProgress) => { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/image-kit-upload', { method: 'POST', body: formData, }); if (!response.ok) { throw new Error('Upload failed'); } const data = await response.json(); return { id: data.fileId, src: data.url, sizes: { width: data.width, height: data.height, }, }; }, delete: async (src) => { await fetch('/api/image-kit-delete', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ src }), }); }, }, }), ]; ``` ## Options ### upload Can be either an **object** (endpoint-based) or a **function** (custom upload): URL endpoint for image upload HTTP method for upload Custom headers for upload request Form field name for the file Maximum file size in bytes Accepted file types (e.g., "image/png,image/jpeg") Callback for upload progress Callback when upload succeeds Callback when upload fails Custom async function for direct uploads to third-party services. **Signature:** ```typescript theme={null} (file: File, onProgress?: (progress: ImageUploadProgress) => void) => Promise ``` **Returns `ImageElementProps`:** * `id` - Unique identifier for the image * `src` - URL of the uploaded image * `sizes` - Object with `width` and `height` ### delete (optional) Can be either an **object** (endpoint-based) or a **function** (custom delete). If not provided, deleting an image block will only remove it from the editor without calling any storage deletion logic. URL endpoint for image deletion HTTP method for deletion Custom headers for delete request Custom async function for deleting images from third-party services. **Signature:** ```typescript theme={null} (src: string) => Promise ``` **Parameters:** * `src` - The URL/source of the image to delete Maximum image width in pixels Maximum image height in pixels Image optimization provider Responsive image sizes ## Element Props Image source URL Alternative text for accessibility Responsive image source set How the image should fit its container Image dimensions: `{ width: number, height: number }` Background color for image container ## Commands ```typescript theme={null} import { ImageCommands } from '@yoopta/image'; // Update image source ImageCommands.updateImage(editor, blockId, { src: 'https://example.com/image.jpg', alt: 'Description', }); // Delete image ImageCommands.deleteImage(editor, blockId); ``` ## Upload Response Format Your upload endpoint should return: ```typescript theme={null} { id: string; url: string; width?: number; height?: number; size?: number; format?: string; } ``` ## Custom Rendering ```jsx theme={null} import { Image } from '@yoopta/image'; const CustomImage = Image.extend({ elements: { image: { render: (props) => { const { src, alt, sizes, fit } = props.element.props; return (
{alt} {alt &&
{alt}
} {props.children}
); }, }, }, }); ``` ## Parsers ### HTML Deserialization The plugin automatically deserializes `` tags: ```html theme={null} Description ``` ### HTML Serialization ```html theme={null}
Description
``` ### Markdown Serialization ```markdown theme={null} ![Description](https://example.com/image.jpg) ``` ## Use Cases Feature images and inline illustrations Screenshots and diagrams Project images and galleries Product images with descriptions ## Best Practices Compress and resize images before uploading to reduce load times Provide descriptive alt text for accessibility Configure appropriate maxWidth and maxHeight for your design Enable lazy loading for better performance Handle upload errors gracefully with user feedback ## Hooks ### useImageUpload Supports both endpoint-based and custom function approaches: ```typescript theme={null} import { useImageUpload } from '@yoopta/image'; const { upload, loading, progress, error } = useImageUpload({ endpoint: '/api/upload', onSuccess: (result) => { console.log('Uploaded:', result.url); }, }); // Upload a file const handleUpload = async (file: File) => { const result = await upload(file); }; ``` ```typescript theme={null} import { useImageUpload } from '@yoopta/image'; const { upload, loading, progress, error } = useImageUpload( async (file, onProgress) => { const formData = new FormData(); formData.append('file', file); const response = await fetch('https://api.cloudinary.com/v1_1/cloud/upload', { method: 'POST', body: formData, }); const data = await response.json(); return { id: data.public_id, src: data.secure_url, sizes: { width: data.width, height: data.height }, }; } ); const handleUpload = async (file: File) => { const result = await upload(file); }; ``` ### useImageDelete Supports both endpoint-based and custom function approaches: ```typescript theme={null} import { useImageDelete } from '@yoopta/image'; const { deleteImage, loading, error } = useImageDelete({ endpoint: '/api/delete', }); ``` ```typescript theme={null} import { useImageDelete } from '@yoopta/image'; const { deleteImage, loading, error } = useImageDelete( async (src) => { await fetch('/api/delete', { method: 'DELETE', body: JSON.stringify({ src }), }); } ); ``` ## Troubleshooting This error occurs when you're using a theme but haven't configured the `upload` option. **Solution:** Add an `upload` configuration to your Image plugin: ```jsx theme={null} Image.extend({ options: { upload: async (file) => { // Your upload logic return { id: '...', src: '...' }; }, }, }) ``` If you want to delete images from your storage when removed from the editor, you need to configure the `delete` option. **Solution:** Add a `delete` configuration to your Image plugin: ```jsx theme={null} Image.extend({ options: { upload: async (file) => { ... }, delete: async (element) => { // Your delete logic await deleteFromStorage(element.props.id); }, }, }) ``` Note: The `delete` option is optional. Without it, images are only removed from the editor. This error occurs when using endpoint-based configuration without providing the `endpoint` URL. **Solution:** Make sure to include the `endpoint` property: ```jsx theme={null} Image.extend({ options: { upload: { endpoint: '/api/upload-image', // Required! }, delete: { endpoint: '/api/delete-image', // Required! }, }, }) ``` ## Related Plugins * [Video Plugin](/plugins/video) - For video content * [File Plugin](/plugins/file) - For file attachments * [Carousel Plugin](/plugins/carousel) - For image galleries # Link Source: https://docs.yoopta.dev/plugins/link Inline links and link blocks ## Overview The Link plugin provides inline links and link blocks for adding hyperlinks to your content. # Lists Source: https://docs.yoopta.dev/plugins/lists Bulleted, numbered, and todo lists ## Overview The Lists plugin provides bulleted lists, numbered lists, and todo (checkable) lists for structured content. # MathInline Source: https://docs.yoopta.dev/plugins/math Inline math expressions rendered with KaTeX ## Overview The MathInline plugin enables users to insert inline math expressions into any text block. Expressions are written in LaTeX and rendered using [KaTeX](https://katex.org/). It works as an `inlineVoid` element, meaning it appears inline alongside regular text but is not directly editable — users edit the LaTeX source through a popover. Part of the `@yoopta/math` package, which also includes the [MathBlock](/plugins/math-block) plugin for standalone display-mode equations. ## Installation ```bash theme={null} npm install @yoopta/math katex ``` You also need KaTeX CSS for proper rendering: ```tsx theme={null} import 'katex/dist/katex.min.css'; ``` ## Basic Usage Pass the plugin to `createYooptaEditor` (wrapped with `withMath`). Plugins and marks are passed to `createYooptaEditor`, not to ``. ```tsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; import { MathInline, withMath } from '@yoopta/math'; import 'katex/dist/katex.min.css'; const plugins = [Paragraph, MathInline]; export default function Editor() { const editor = useMemo(() => withMath(createYooptaEditor({ plugins, marks: [] })), []); return ; } ``` To use both inline and block math together: ```tsx theme={null} import { MathInline, MathBlock, withMath } from '@yoopta/math'; const plugins = [Paragraph, MathInline, MathBlock]; ``` ## Features * Inline math expressions rendered with KaTeX * Works inside any text block (Paragraph, Headings, Callout, Lists, etc.) * Live preview while editing LaTeX source * Copy LaTeX source to clipboard * Toolbar integration — select text and convert to math expression * HTML, Markdown, and email serialization * Dark mode support ## Using with Themes The `@yoopta/themes-shadcn` package includes a styled MathInline component with a HoverCard for preview and editing. ```tsx theme={null} import { applyTheme } from '@yoopta/themes-shadcn'; import { MathInline, MathBlock, withMath } from '@yoopta/math'; import Paragraph from '@yoopta/paragraph'; const plugins = applyTheme([Paragraph, MathInline, MathBlock]); const editor = withMath(createYooptaEditor({ plugins, marks: MARKS })); ``` The shadcn theme provides: * **Preview mode** — rendered math, LaTeX source display, copy and edit buttons * **Edit mode** — live preview, LaTeX textarea input, save/cancel/delete actions You can also apply the theme to just the MathInline plugin: ```tsx theme={null} import { MathInline } from '@yoopta/math'; import { MathInlineUI } from '@yoopta/themes-shadcn/math'; const StyledMathInline = MathInline.extend({ elements: MathInlineUI }); ``` ## Element Props | Prop | Type | Description | | ---------- | -------- | ------------------------------------- | | `latex` | `string` | The LaTeX expression string | | `nodeType` | `string` | Always `'inlineVoid'` for this plugin | ## Commands Import commands: `import { MathInlineCommands } from '@yoopta/math'` ### `buildMathInlineElement` Creates a math inline element structure without inserting it. ```tsx theme={null} const element = MathInlineCommands.buildMathInlineElement(editor, { props: { latex: 'x^2 + y^2 = r^2' }, }); ``` ### `insertMathInline` Inserts a math inline element at the current Slate selection. ```tsx theme={null} import { Blocks } from '@yoopta/editor'; const slate = Blocks.getBlockSlate(editor, { id: blockId }); MathInlineCommands.insertMathInline(editor, 'E = mc^2', { slate }); ``` If text is selected, the selection is replaced with the math element. ### `updateMathInline` Updates the LaTeX expression of an existing math element. ```tsx theme={null} MathInlineCommands.updateMathInline(editor, elementId, '\\frac{a}{b}', { blockId: 'block-1', }); ``` ### `deleteMathInline` Removes a math inline element by its ID. ```tsx theme={null} MathInlineCommands.deleteMathInline(editor, elementId, { blockId: 'block-1', }); ``` ### `findMathInlineElements` Finds all math inline elements in a block. ```tsx theme={null} const elements = MathInlineCommands.findMathInlineElements(editor, { blockId: 'block-1', }); ``` ### `openEditor` / `closeEditor` Controls the edit popover state (requires `withMath` extension). ```tsx theme={null} MathInlineCommands.openEditor(editor, { element: mathElement, blockId: 'block-1', anchorEl: domNode, }); MathInlineCommands.closeEditor(editor); ``` ## Editor Extension The `withMath` function extends the editor with a `math` namespace for managing the edit popover state. This extension is shared between MathInline and MathBlock. ```tsx theme={null} import { withMath } from '@yoopta/math'; const editor = withMath(createYooptaEditor({ plugins, marks })); // Access state editor.math.state; // { isOpen, editingElement, blockId, anchorEl } // Open/close programmatically editor.math.open({ element, blockId, anchorEl }); editor.math.close(); ``` ### `useMathState` Hook Subscribe to math state changes in React components (must be inside ``). ```tsx theme={null} import { useMathState } from '@yoopta/math'; function MyComponent() { const { isOpen, editingElement, blockId } = useMathState(); // ... } ``` ## Toolbar Integration Add a math button to the floating toolbar that converts selected text into a math expression: ```tsx theme={null} import { Blocks, useYooptaEditor } from '@yoopta/editor'; import { MathInlineCommands } from '@yoopta/math'; import { Editor, Range } from 'slate'; import { SigmaIcon } from 'lucide-react'; function MathToolbarButton() { const editor = useYooptaEditor(); const onInsertMath = () => { if (editor.path.current === null) return; const currentBlockId = Object.keys(editor.children).find( (id) => editor.children[id]?.meta.order === editor.path.current, ); if (!currentBlockId) return; const slate = Blocks.getBlockSlate(editor, { id: currentBlockId }); if (!slate || !slate.selection) return; // Use selected text as LaTeX, or fall back to placeholder const selectedText = !Range.isCollapsed(slate.selection) ? Editor.string(slate, slate.selection) : ''; MathInlineCommands.insertMathInline(editor, selectedText || 'E = mc^2', { slate }); }; return ( ); } ``` ## Parsers ### HTML Serializes to `` with KaTeX-rendered content inside. Deserializes from `` elements, reading the `data-latex` attribute. ### Markdown Serializes to standard inline math syntax: `$E = mc^2$` ### Email Serializes with KaTeX HTML output wrapped in a serif font span for email clients. ## LaTeX Examples Common expressions to use with the plugin: | Expression | LaTeX | | ------------- | ---------------------------------------------- | | Fraction | `\frac{a}{b}` | | Square root | `\sqrt{x}` | | Superscript | `x^2` | | Subscript | `x_i` | | Summation | `\sum_{i=1}^{n} x_i` | | Integral | `\int_0^1 f(x) dx` | | Greek letters | `\alpha, \beta, \gamma` | | Matrix | `\begin{pmatrix} a & b \\ c & d \end{pmatrix}` | ## Related Plugins Display-mode math equations as standalone blocks @mentions (another inlineVoid element plugin) # MathBlock Source: https://docs.yoopta.dev/plugins/math-block Display-mode math equations as standalone blocks ## Overview The MathBlock plugin provides standalone, display-mode math equations rendered with [KaTeX](https://katex.org/). Unlike [MathInline](/plugins/math), which appears inline within text, MathBlock creates a dedicated block for larger equations that are centered and rendered in display mode. Both plugins are part of the `@yoopta/math` package, following the same pattern as `Code` / `CodeGroup` from `@yoopta/code`. ## Installation ```bash theme={null} npm install @yoopta/math katex ``` You also need KaTeX CSS for proper rendering: ```tsx theme={null} import 'katex/dist/katex.min.css'; ``` ## Basic Usage Pass the plugin to `createYooptaEditor` (wrapped with `withMath`). ```tsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; import { MathBlock, withMath } from '@yoopta/math'; import 'katex/dist/katex.min.css'; const plugins = [Paragraph, MathBlock]; export default function Editor() { const editor = useMemo(() => withMath(createYooptaEditor({ plugins, marks: [] })), []); return ; } ``` To use both inline and block math together: ```tsx theme={null} import { MathInline, MathBlock, withMath } from '@yoopta/math'; const plugins = [Paragraph, MathInline, MathBlock]; ``` ## Features * Display-mode equations rendered with KaTeX (centered, larger font) * Standalone block — not inline within text * Click to edit, with live KaTeX preview * Copy LaTeX source to clipboard * HTML, Markdown, and email serialization * Dark mode support ## Using with Themes The `@yoopta/themes-shadcn` package includes a styled MathBlock component with a card-based edit UI. ```tsx theme={null} import { applyTheme } from '@yoopta/themes-shadcn'; import { MathInline, MathBlock, withMath } from '@yoopta/math'; import Paragraph from '@yoopta/paragraph'; const plugins = applyTheme([Paragraph, MathInline, MathBlock]); const editor = withMath(createYooptaEditor({ plugins, marks: MARKS })); ``` The shadcn theme provides: * **Preview mode** — centered display-mode rendering with hover overlay (copy + edit buttons) * **Edit mode** — larger textarea with live preview, save/cancel/delete actions You can also apply the theme to just the MathBlock plugin: ```tsx theme={null} import { MathBlock } from '@yoopta/math'; import { MathBlockUI } from '@yoopta/themes-shadcn/math'; const StyledMathBlock = MathBlock.extend({ elements: MathBlockUI }); ``` ## Element Props | Prop | Type | Description | | ---------- | -------- | ------------------------------- | | `latex` | `string` | The LaTeX expression string | | `nodeType` | `string` | Always `'void'` for this plugin | ## Commands Import commands: `import { MathBlockCommands } from '@yoopta/math'` ### `buildMathBlockElement` Creates a math block element structure without inserting it. ```tsx theme={null} const element = MathBlockCommands.buildMathBlockElement(editor, { props: { latex: '\\int_0^\\infty e^{-x} dx = 1' }, }); ``` ### `updateMathBlock` Updates the LaTeX expression of an existing math block. ```tsx theme={null} MathBlockCommands.updateMathBlock(editor, blockId, '\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}'); ``` ### `getMathBlockElement` Finds the math-block element in a given block. ```tsx theme={null} const element = MathBlockCommands.getMathBlockElement(editor, { blockId: 'block-1', }); ``` ### `openEditor` / `closeEditor` Controls the edit state (requires `withMath` extension). Shared with MathInline. ```tsx theme={null} MathBlockCommands.openEditor(editor, { element: mathBlockElement, blockId: 'block-1', anchorEl: domNode, }); MathBlockCommands.closeEditor(editor); ``` ## Editor Extension MathBlock shares the same `withMath` extension as MathInline. See the [MathInline docs](/plugins/math#editor-extension) for details. ```tsx theme={null} import { withMath } from '@yoopta/math'; const editor = withMath(createYooptaEditor({ plugins, marks })); ``` ## Parsers ### HTML Serializes to `
` with KaTeX-rendered content inside (display mode). Deserializes from `
` elements, reading the `data-latex` attribute. ### Markdown Serializes to block math syntax: ``` $$ E = mc^2 $$ ``` ### Email Serializes with display-mode KaTeX HTML output wrapped in a centered div for email clients. ## LaTeX Examples Common display-mode expressions: | Expression | LaTeX | | ----------------- | -------------------------------------------------------- | | Integral | `\int_0^\infty e^{-x} dx = 1` | | Summation | `\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}` | | Matrix | `\begin{pmatrix} a & b \\ c & d \end{pmatrix}` | | Aligned equations | `\begin{aligned} x &= a + b \\ y &= c + d \end{aligned}` | | Limit | `\lim_{x \to \infty} \frac{1}{x} = 0` | | Binomial | `\binom{n}{k} = \frac{n!}{k!(n-k)!}` | ## Related Plugins Inline math expressions within text Code blocks (another void block plugin) # Mention Source: https://docs.yoopta.dev/plugins/mention Mention users, channels, pages, or any custom resources with autocomplete ## Overview The Mention plugin enables users to mention people, channels, pages, or any custom resources by typing a trigger character (default: `@`). It provides an autocomplete dropdown with search functionality and customizable rendering. ## Installation ```bash theme={null} npm install @yoopta/mention ``` ## Basic Usage Pass the plugin to `createYooptaEditor` (wrapped with `withMentions`); do not pass `plugins` to ``. Add `MentionDropdown` as a child when using theme UI. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import { Mention, withMentions } from '@yoopta/mention'; import { MentionDropdown } from '@yoopta/themes-shadcn/mention'; const plugins = [ Mention.extend({ options: { onSearch: async (query) => { const results = await fetchUsers(query); return results; }, }, }), ]; export default function Editor() { const editor = useMemo( () => withMentions(createYooptaEditor({ plugins, marks: [] })), [], ); return ( {}}> ); } ``` **Required Configuration** You **must** configure the `onSearch` option. Without this option, the mention dropdown won't work. ```jsx theme={null} const plugins = [ Mention.extend({ options: { onSearch: async (query, trigger) => { // Your search logic here return [ { id: '1', name: 'John Doe', avatar: '...', type: 'user' }, // ... more results ]; }, }, }), ]; ``` See the [Configuration](#configuration) section below for detailed examples. ## Features * **Multiple Triggers**: Support for multiple trigger characters (e.g., `@` for users, `#` for channels) * **Autocomplete Dropdown**: Searchable dropdown with keyboard navigation * **Custom Search**: Implement your own search logic (API calls, local filtering, etc.) * **Type Support**: Categorize mentions by type (user, channel, page, custom) * **Rich Metadata**: Store additional data with each mention (avatar, email, URL, etc.) * **Hover Cards**: Display mention details on hover (when using themes) * **Keyboard Navigation**: Arrow keys, Enter, Escape support * **Debounced Search**: Configurable debounce delay for search requests ## Configuration ### Basic Configuration ```jsx theme={null} import { Mention } from '@yoopta/mention'; import { withMentions } from '@yoopta/mention'; const plugins = [ Mention.extend({ options: { onSearch: async (query, trigger) => { // Search users const response = await fetch(`/api/users?q=${query}`); const users = await response.json(); return users.map((user) => ({ id: user.id, name: user.name, avatar: user.avatar, type: 'user', meta: { email: user.email, role: user.role, }, })); }, }, }), ]; ``` ### Multiple Triggers You can configure multiple trigger characters for different types of mentions: ```jsx theme={null} const plugins = [ Mention.extend({ options: { triggers: [ { char: '@', type: 'user' }, { char: '#', type: 'channel' }, { char: '[[', type: 'page' }, ], onSearch: async (query, trigger) => { if (trigger.type === 'user') { return await searchUsers(query); } else if (trigger.type === 'channel') { return await searchChannels(query); } else if (trigger.type === 'page') { return await searchPages(query); } return []; }, }, }), ]; ``` ### Advanced Configuration ```jsx theme={null} const plugins = [ Mention.extend({ options: { triggers: [ { char: '@', type: 'user', allowSpaces: false, // Don't allow spaces in search query allowedAfter: /\s|^/, // Only trigger after whitespace or start }, ], onSearch: async (query, trigger) => { // Your search logic return results; }, debounceMs: 300, // Debounce search by 300ms minQueryLength: 1, // Minimum query length before searching closeOnSelect: true, // Close dropdown when item is selected closeOnClickOutside: true, // Close on click outside closeOnEscape: true, // Close on Escape key onSelect: (item, trigger) => { console.log('Selected:', item, trigger); }, onOpen: (trigger) => { console.log('Dropdown opened:', trigger); }, onClose: () => { console.log('Dropdown closed'); }, }, }), ]; ``` ## Options Array of trigger configurations. Each trigger defines a character(s) that opens the mention dropdown. Character(s) that trigger the mention dropdown (e.g., `'@'`, `'#'`, `'[['`) Optional type to categorize this trigger (e.g., `'user'`, `'channel'`, `'page'`) Allow spaces in search query (default: false) Pattern that must precede the trigger (default: whitespace or start of line) Simple single trigger character (shorthand for `triggers: [{char}]`). Use this for a single trigger, or use `triggers` array for multiple triggers. Search function called when user types after trigger. **Signature:** ```typescript theme={null} (query: string, trigger: MentionTrigger) => Promise; ``` **Parameters:** * `query` - The search query (without trigger char) * `trigger` - The trigger that opened the dropdown **Returns:** Promise resolving to array of `MentionItem` objects Debounce delay for search in milliseconds. Reduces API calls while user is typing. Minimum query length before triggering search. Set to `1` or higher to avoid searching on empty query. Called when a mention is selected. **Signature:** ```typescript theme={null} (item: MentionItem, trigger: MentionTrigger) => void ``` Called when dropdown opens. **Signature:** ```typescript theme={null} (trigger: MentionTrigger) => void ``` Called when dropdown closes. **Signature:** ```typescript theme={null} () => void ``` Close dropdown when item is selected. Close dropdown on click outside. Close dropdown on Escape key. ## Element Props Unique identifier for the mention Display name of the mention (e.g., "John Doe") URL to avatar image Type of mention (e.g., `'user'`, `'channel'`, `'page'`, `'custom'`) Additional metadata for the mention. Can contain any custom fields: ```typescript theme={null} { email?: string; url?: string; description?: string; [key: string]: unknown; } ``` ## Commands ```typescript theme={null} import { MentionCommands } from '@yoopta/mention'; // Insert a mention MentionCommands.insertMention(editor, { id: '123', name: 'John Doe', avatar: 'https://example.com/avatar.png', type: 'user', meta: { email: 'john@example.com', }, }); // Find mentions const allMentions = MentionCommands.findMentions(editor); const userMentions = MentionCommands.findMentionsByType(editor, 'user'); const specificMention = MentionCommands.findMention(editor, '123'); // Update mention MentionCommands.updateMention(editor, '123', { name: 'Jane Doe', avatar: 'https://example.com/new-avatar.png', }); // Delete mention MentionCommands.deleteMention(editor, '123'); // Dropdown control MentionCommands.openDropdown(editor, { trigger: { char: '@', type: 'user' }, targetRect: { domRect, clientRects }, triggerRange: { blockId, path, startOffset }, }); MentionCommands.closeDropdown(editor, 'manual'); // State const state = MentionCommands.getState(editor); const query = MentionCommands.getQuery(editor); const trigger = MentionCommands.getTrigger(editor); ``` ## Using with Themes When using a theme (e.g., `@yoopta/themes-shadcn`), you need to: 1. **Apply the theme** to your plugins 2. **Add the MentionDropdown component** to your editor ```jsx theme={null} import { Mention } from '@yoopta/mention'; import { withMentions } from '@yoopta/mention'; import { withShadcnUI } from '@yoopta/themes-shadcn'; import { MentionDropdown } from '@yoopta/themes-shadcn/mention'; const plugins = withShadcnUI([ Mention.extend({ options: { onSearch: async (query, trigger) => { // Your search logic return results; }, }, }), // ... other plugins ]); const editor = withMentions( createYooptaEditor({ plugins, // ... }), ); {/* Add this component */} {/* ... other UI components */} ; ``` ## Custom Rendering You can customize how mentions are rendered: ```jsx theme={null} import { Mention } from '@yoopta/mention'; const CustomMention = Mention.extend({ elements: { mention: { render: (props) => { const { id, name, avatar, type } = props.element.props; return ( {avatar && {name}} @{name} {type && {type}} {props.children} ); }, }, }, }); ``` ## Custom Dropdown You can create a custom dropdown using the `useMentionDropdown` hook: ```jsx theme={null} import { useMentionDropdown } from '@yoopta/mention'; import { MentionDropdown } from '@yoopta/themes-shadcn/mention'; function CustomMentionDropdown() { const { isOpen, query, trigger, items, loading, selectedIndex, selectItem, refs, floatingStyles, } = useMentionDropdown(); if (!isOpen) return null; return (
{loading &&
Loading...
} {items.map((item, index) => (
selectItem(item)}> {item.avatar && {item.name}} {item.name}
))}
); } ``` ## Parsers ### HTML Deserialization The plugin automatically deserializes mention spans: ```html theme={null} @John Doe ``` ### HTML Serialization ```html theme={null} @John Doe ``` ### Markdown Serialization ```markdown theme={null} @John Doe ``` ### Email Serialization ```html theme={null} @John Doe ``` ## Use Cases Mention team members in comments or posts Reference channels or topics with # Link to pages or documents with \[\[ Mention any custom resource type ## Best Practices Use the `debounceMs` option to reduce API calls while users are typing. A value of 300ms is usually optimal. Use `minQueryLength: 1` or higher to avoid searching on empty queries, which can be expensive. Show loading indicators in your custom dropdown while search is in progress. Handle search errors gracefully and provide user feedback. Return a reasonable number of results (e.g., 10-20) to avoid overwhelming users. Use the `type` field to categorize mentions and enable different search logic per type. ## Hooks ### useMentionDropdown Hook for building custom mention dropdowns: ```typescript theme={null} import { useMentionDropdown } from '@yoopta/mention'; const { // State isOpen, query, trigger, // Results items, loading, error, // Navigation selectedIndex, setSelectedIndex, // Actions selectItem, close, // Refs for floating-ui refs, floatingStyles, } = useMentionDropdown({ debounceMs: 300, // Optional override }); ``` ## Troubleshooting Make sure you've: 1. Wrapped your editor with `withMentions()` 2. Configured the `onSearch` option 3. Added the `MentionDropdown` component to your editor (when using themes) **Solution:** ```jsx theme={null} const editor = withMentions(createYooptaEditor({ ... })); ``` Check that your `onSearch` function: 1. Returns a Promise 2. Returns an array of `MentionItem` objects 3. Each item has `id` and `name` properties **Solution:** ```jsx theme={null} onSearch: async (query, trigger) => { const results = await fetchResults(query); return results.map((item) => ({ id: item.id, // Required name: item.name, // Required avatar: item.avatar, // Optional type: item.type, // Optional meta: item.meta, // Optional })); }, ``` Make sure the mention is being inserted in a block that supports inline elements (e.g., Paragraph, Heading). **Solution:** Mentions are inline elements and can only be inserted in blocks that support inline content. When using multiple triggers, make sure you're using the `triggers` array instead of the `char` option. **Solution:** ```jsx theme={null} options: { triggers: [ { char: '@', type: 'user' }, { char: '#', type: 'channel' }, ], // Don't use 'char' when using 'triggers' } ``` ## Related Plugins * [Link Plugin](/plugins/link) - For hyperlinks * [Paragraph Plugin](/plugins/paragraph) - For text blocks that support mentions # Paragraph Source: https://docs.yoopta.dev/plugins/paragraph Basic text blocks for content creation ## Overview The Paragraph plugin is the fundamental building block of any document. It provides basic text formatting and should always be included in your editor configuration. ## Installation ```bash theme={null} npm install @yoopta/paragraph ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Paragraph from '@yoopta/paragraph'; const plugins = [Paragraph]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Plain Text**: Write unformatted text * **Text Marks**: Supports bold, italic, underline, etc. * **Alignment**: Left, center, right alignment * **Keyboard Shortcuts**: Type `p` or `text` to insert * **Default Block**: Typically the default block type * **Indentation**: Support for nested indentation ## Configuration The Paragraph plugin works out of the box with minimal configuration: ```jsx theme={null} import { Paragraph } from '@yoopta/paragraph'; const plugins = [Paragraph]; ``` ## Options Display title in UI menus Description shown in menus Keyboard shortcuts to trigger the plugin ## Commands Use the **`Blocks`** namespace from `@yoopta/editor`: ```typescript theme={null} import { Blocks } from '@yoopta/editor'; // Transform current block to a Paragraph block Blocks.toggleBlock(editor, 'Paragraph', { preserveContent: true }); // Insert a new paragraph block Blocks.insertBlock(editor, 'Paragraph', { at: editor.path.current, focus: true }); // Update paragraph alignment (block meta) Blocks.updateBlock(editor, blockId, { meta: { align: 'center' }, }); ``` ## Custom Rendering ```jsx theme={null} import { Paragraph } from '@yoopta/paragraph'; const CustomParagraph = Paragraph.extend({ elements: { paragraph: { render: (props) => { return (

{props.children}

); }, }, }, }); ``` ### With Custom Styling ```jsx theme={null} const StyledParagraph = Paragraph.extend({ elements: { paragraph: { render: (props) => { const align = props.blockData?.meta?.align || 'left'; return (

{props.children}

); }, }, }, }); ``` ## Parsers ### HTML Deserialization The plugin automatically deserializes `

` tags: ```html theme={null}

This is a paragraph with bold and italic text.

``` ### HTML Serialization ```html theme={null}

Your paragraph content

``` ### Markdown Serialization ```markdown theme={null} Your paragraph content ``` ### Email Serialization The plugin provides email-compatible HTML with inline styles: ```html theme={null}

Your paragraph content

``` ## Text Marks The Paragraph plugin supports all standard text marks: * **Bold**: Ctrl/Cmd + B * **Italic**: Ctrl/Cmd + I * **Underline**: Ctrl/Cmd + U * **Strikethrough**: Ctrl/Cmd + Shift + S * **Code**: Ctrl/Cmd + E * **Links**: Ctrl/Cmd + K ## Block Metadata Each paragraph block can have metadata: ```typescript theme={null} { align: 'left' | 'center' | 'right', depth: number, // Indentation level } ``` ## Use Cases Main body text for articles and posts Descriptive text in technical docs Marketing copy and descriptions Quick notes and text entries ## Best Practices The Paragraph plugin should always be included as it's typically the default block type Configure Paragraph as the default block type for new content The Paragraph plugin should remain simple - use other plugins for complex formatting Use text marks (bold, italic) for inline formatting within paragraphs ## Extensions The Paragraph plugin includes custom Slate extensions for normalized behavior: ### withParagraph ```typescript theme={null} import { withParagraph } from '@yoopta/paragraph'; // Applied automatically by the plugin const slate = withParagraph(createEditor()); ``` This extension handles: * Text normalization * Empty paragraph handling * Consistent whitespace behavior ## Advanced Patterns ### With Drop Caps ```jsx theme={null} const DropCapParagraph = Paragraph.extend({ elements: { paragraph: { render: (props) => { const hasDropCap = props.element.props?.dropCap; return (

{props.children}

); }, }, }, }); ``` ```css theme={null} .drop-cap::first-letter { float: left; font-size: 3em; line-height: 0.9; margin: 0 0.1em 0 0; } ``` ### With Character Count ```jsx theme={null} const CountedParagraph = Paragraph.extend({ elements: { paragraph: { render: (props) => { const text = props.element.children.map((n) => n.text).join(''); const count = text.length; return (

{props.children}

{count} characters
); }, }, }, }); ``` ## Related Plugins * [Headings Plugin](/plugins/headings) - For titles and headings * [Blockquote Plugin](/plugins/blockquote) - For quoted text * [Code Plugin](/plugins/code) - For code blocks # Steps Source: https://docs.yoopta.dev/plugins/steps Create step-by-step instructions and tutorials ## Overview The Steps plugin allows you to create numbered, step-by-step instructions. It's perfect for tutorials, guides, and any content that requires a sequential flow. ## Installation ```bash theme={null} npm install @yoopta/steps ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Steps from '@yoopta/steps'; const plugins = [Steps]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Automatic Numbering**: Steps are numbered automatically * **Nested Content**: Each step has a heading and content area * **Keyboard Shortcuts**: Type `steps` to insert * **Easy Navigation**: Add and remove steps easily * **Customizable**: Full control over styling ## Structure The Steps plugin consists of nested elements: ``` step-container └── step-list └── step-list-item (multiple) ├── step-list-item-heading └── step-list-item-content ``` ## Configuration ```jsx theme={null} import { StepsPlugin } from '@yoopta/steps'; const plugins = [ StepsPlugin, ]; ``` ## Options Display title in UI Description shown in menus Keyboard shortcuts to trigger the plugin ## Commands ### buildStepsElements Creates the initial steps structure. ```typescript theme={null} import { StepsCommands } from '@yoopta/steps'; StepsCommands.buildStepsElements(editor, { items: 3 }); ``` Number of steps to create initially ### addStep Add a new step after the current one. ```typescript theme={null} StepsCommands.addStep(editor, { blockId: 'block-id', at: selection, }); ``` ### deleteStep Remove a specific step. ```typescript theme={null} StepsCommands.deleteStep(editor, { blockId: 'block-id', stepId: 'step-id', }); ``` ## Keyboard Behavior * **Enter on Heading**: Creates a new step * **Enter in Content**: Inserts newline within content * **Backspace on Heading**: Deletes the step (when empty) * **Backspace in Content**: Prevents deletion at content start ## Initial Structure When created, the plugin initializes with 2 steps: ```typescript theme={null} { steps: [ { heading: 'Step 1 heading', content: 'Step 1 content', }, { heading: 'Step 2 heading', content: 'Step 2 content', }, ], } ``` ## Custom Rendering ```jsx theme={null} import { StepsPlugin } from '@yoopta/steps'; const CustomSteps = StepsPlugin.extend({ elements: { 'step-container': { render: (props) => (
{props.children}
), }, 'step-list': { render: (props) => (
    {props.children}
), }, 'step-list-item': { render: (props) => (
  • {props.children}
  • ), }, 'step-list-item-heading': { render: (props) => (

    {props.children}

    ), }, 'step-list-item-content': { render: (props) => (
    {props.children}
    ), }, }, }); ``` ### With Custom Numbering ```jsx theme={null} const CustomSteps = StepsPlugin.extend({ elements: { 'step-list-item': { render: (props) => { // Get step index from path or calculate const stepNumber = getStepNumber(props); return (
  • {stepNumber}
    {props.children}
  • ); }, }, }, }); ``` ## Styling Examples ### Basic CSS ```css theme={null} .steps-container { counter-reset: step-counter; } .step-item { counter-increment: step-counter; position: relative; padding-left: 3rem; } .step-item::before { content: counter(step-counter); position: absolute; left: 0; top: 0; width: 2rem; height: 2rem; background: #3b82f6; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; } ``` ### With Progress Line ```css theme={null} .step-list { position: relative; } .step-list::before { content: ''; position: absolute; left: 1rem; top: 2rem; bottom: 2rem; width: 2px; background: #e5e7eb; } .step-item { position: relative; padding-left: 4rem; padding-bottom: 2rem; } ``` ## Use Cases Step-by-step coding or software tutorials Cooking instructions with numbered steps Software installation procedures Assembly or craft project steps ## Best Practices Use concise, action-oriented headings (e.g., "Install dependencies") Provide sufficient detail in content area for each step Ensure steps follow a logical, sequential order Include expected outcomes or tips within steps Combine with Code plugin for technical steps ## Advanced Patterns ### With Conditional Steps ```jsx theme={null} const ConditionalSteps = StepsPlugin.extend({ elements: { 'step-list-item': { props: { condition: null, // e.g., "macOS", "Windows" }, render: (props) => { const { condition } = props.element.props; return (
  • {condition && ( {condition} )} {props.children}
  • ); }, }, }, }); ``` ### With Completion Tracking ```jsx theme={null} const TrackableSteps = StepsPlugin.extend({ elements: { 'step-list-item': { props: { completed: false, }, render: (props) => { const [completed, setCompleted] = useState( props.element.props.completed ); return (
  • setCompleted(!completed)} /> {props.children}
  • ); }, }, }, }); ``` ## Related Plugins * [Accordion Plugin](/plugins/accordion) - For collapsible sections * [Tabs Plugin](/plugins/tabs) - For tabbed content * [Code Plugin](/plugins/code) - For code examples in steps # Table Source: https://docs.yoopta.dev/plugins/table Create and manage tabular data ## Overview The Table plugin provides a complete solution for creating and managing tables. It includes features for cell selection, merging, splitting, and formatting tabular data. ## Installation ```bash theme={null} npm install @yoopta/table ``` ## Basic Usage Pass the plugin to `createYooptaEditor`; do not pass `plugins` to ``. ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import Table from '@yoopta/table'; const plugins = [Table]; export default function Editor() { const editor = useMemo(() => createYooptaEditor({ plugins, marks: [] }), []); return {}} />; } ``` ## Features * **Flexible Tables**: Create tables of any size * **Cell Operations**: Merge, split, insert, delete cells * **Header Rows/Columns**: Mark rows and columns as headers * **Cell Alignment**: Horizontal and vertical alignment * **Column Resizing**: Adjustable column widths * **Cell Selection**: Select multiple cells * **Keyboard Shortcuts**: Type `table`, `||`, or `3x3` to insert ## Structure The Table plugin consists of nested elements: ``` table (props: headerRow, headerColumn, columnWidths) └── table-row (multiple) └── table-data-cell (props: align, verticalAlign, colSpan, rowSpan, asHeader) ``` ## Configuration ```jsx theme={null} import { Table } from '@yoopta/table'; const plugins = [ Table, ]; ``` ## Options Display title in UI Description shown in menus Keyboard shortcuts to trigger the plugin ## Element Props ### table Whether the first row should be treated as a header Whether the first column should be treated as a header Array of column widths in pixels ### table-data-cell Whether this cell should be rendered as a header (th) Horizontal text alignment Vertical text alignment Number of columns this cell spans Number of rows this cell spans Cell background color ## Commands ### buildTableElements Creates the initial table structure. ```typescript theme={null} import { TableCommands } from '@yoopta/table'; TableCommands.buildTableElements(editor, { rows: 3, columns: 3 }); ``` Number of rows to create Number of columns to create ### insertRow Insert a new row at a specific position. ```typescript theme={null} TableCommands.insertRow(editor, blockId, { at: rowIndex, position: 'after', // or 'before' }); ``` ### insertColumn Insert a new column at a specific position. ```typescript theme={null} TableCommands.insertColumn(editor, blockId, { at: columnIndex, position: 'after', // or 'before' }); ``` ### deleteRow Delete a specific row. ```typescript theme={null} TableCommands.deleteRow(editor, blockId, rowIndex); ``` ### deleteColumn Delete a specific column. ```typescript theme={null} TableCommands.deleteColumn(editor, blockId, columnIndex); ``` ### mergeCells Merge selected cells into one. ```typescript theme={null} TableCommands.mergeCells(editor, blockId, { startRow: 0, startCol: 0, endRow: 1, endCol: 1, }); ``` ### splitCell Split a merged cell. ```typescript theme={null} TableCommands.splitCell(editor, blockId, { row: rowIndex, col: columnIndex, }); ``` ### updateCellProps Update cell properties. ```typescript theme={null} TableCommands.updateCellProps(editor, blockId, { row: rowIndex, col: columnIndex, props: { align: 'center', backgroundColor: '#f3f4f6', }, }); ``` ### clearContents Clear contents of selected cells. ```typescript theme={null} import { clearContents } from '@yoopta/table'; clearContents(editor, blockId, selectedCells); ``` ## Initial Structure When created, the plugin initializes with a 3x3 table: ```typescript theme={null} { table: { headerRow: false, headerColumn: false, columnWidths: [200, 150, 250], }, rows: [ // 3 rows { cells: [ // 3 cells per row { align: 'left', verticalAlign: 'top', colSpan: 1, rowSpan: 1, }, ], }, ], } ``` ## Custom Rendering ```jsx theme={null} import { Table } from '@yoopta/table'; const CustomTable = Table.extend({ elements: { table: { render: (props) => { const { headerRow, headerColumn, columnWidths } = props.element.props; return (
    {props.children}
    ); }, }, 'table-row': { render: (props) => ( {props.children} ), }, 'table-data-cell': { render: (props) => { const { asHeader, align, verticalAlign, colSpan, rowSpan, backgroundColor, } = props.element.props; const CellTag = asHeader ? 'th' : 'td'; return ( {props.children} ); }, }, }, }); ``` ## Parsers ### HTML Deserialization The plugin automatically deserializes `` tags: ```html theme={null}
    Cell 1 Cell 2
    Cell 3 Cell 4
    ``` ### HTML Serialization ```html theme={null}
    Cell content
    ``` ### Markdown Serialization ```markdown theme={null} | Column 1 | Column 2 | Column 3 | |----------|----------|----------| | Cell 1 | Cell 2 | Cell 3 | | Cell 4 | Cell 5 | Cell 6 | ``` ## Keyboard Behavior The Table plugin has special keyboard handling: * **Tab**: Move to next cell * **Shift+Tab**: Move to previous cell * **Arrow Keys**: Navigate between cells * **Enter**: Move to cell below * **Backspace**: Delete cell content (not the cell) ## Cell Selection The plugin supports multi-cell selection: ```typescript theme={null} // Access selected cells import { TABLE_CELLS_IN_SELECTION, TABLE_SLATE_TO_SELECTION_SET, } from '@yoopta/table'; const selectedCells = TABLE_CELLS_IN_SELECTION.get(slate); const selectionSet = TABLE_SLATE_TO_SELECTION_SET.get(slate); ``` ## Use Cases Display structured data and statistics Compare pricing plans and features Create timetables and schedules Compare products, services, or features ## Best Practices Always use header rows for better readability and accessibility Avoid overly complex tables with too many merged cells Consider mobile layouts for wide tables Use consistent alignment patterns within columns Provide table captions or descriptions for context ## Styling Examples ### Basic CSS ```css theme={null} .table-wrapper { overflow-x: auto; } .custom-table { width: 100%; border-collapse: collapse; } .custom-table th, .custom-table td { padding: 0.75rem; border: 1px solid #e5e7eb; } .custom-table th { background: #f9fafb; font-weight: 600; text-align: left; } .custom-table tr:hover td { background: #f9fafb; } ``` ### Striped Rows ```css theme={null} .custom-table tbody tr:nth-child(even) { background: #f9fafb; } ``` ### Bordered Style ```css theme={null} .custom-table { border: 2px solid #e5e7eb; } .custom-table th { border-bottom: 2px solid #e5e7eb; } ``` ## Advanced Patterns ### With Cell Selection Highlight ```jsx theme={null} const SelectableTable = Table.extend({ elements: { 'table-data-cell': { render: (props) => { const isSelected = checkIfSelected(props.element); return ( {props.children} ); }, }, }, }); ``` ### With Sortable Columns ```jsx theme={null} const SortableTable = Table.extend({ elements: { 'table-data-cell': { render: (props) => { if (props.element.props.asHeader) { return ( ); } return {props.children}; }, }, }, }); ``` ## Related Plugins * [Paragraph Plugin](/plugins/paragraph) - For text within cells * [Code Plugin](/plugins/code) - For code in cells # Table of Contents Source: https://docs.yoopta.dev/plugins/table-of-contents Void block that renders a navigable list of document headings ## Overview The Table of Contents plugin inserts a block that automatically collects all heading blocks (H1, H2, H3) from the document and renders them as a list. Clicking an item focuses the corresponding heading block and scrolls it into view. The plugin does not ship UI by default—use a theme such as `@yoopta/themes-shadcn` for the full interface. ## Installation ```bash theme={null} npm install @yoopta/table-of-contents ``` ## Basic Usage ```jsx theme={null} import { useMemo } from 'react'; import YooptaEditor, { createYooptaEditor } from '@yoopta/editor'; import TableOfContents from '@yoopta/table-of-contents'; import { HeadingOne, HeadingTwo, HeadingThree } from '@yoopta/headings'; import Paragraph from '@yoopta/paragraph'; import { applyTheme } from '@yoopta/themes-shadcn'; const plugins = applyTheme([ TableOfContents, HeadingOne, HeadingTwo, HeadingThree, Paragraph, ]); export default function Editor() { const editor = useMemo( () => createYooptaEditor({ plugins, marks: [], value: {} }), [], ); return ( {}}> ); } ``` ## Features * **Void block**: No editable text; content is derived from other blocks. * **Auto-generated list**: Collects blocks by type (e.g. `HeadingOne`, `HeadingTwo`, `HeadingThree`) and order. * **Depth control**: Show only H1, or H1+H2, or H1+H2+H3 via the `depth` prop. * **Click to navigate**: Focus and scroll to the heading block when an item is clicked. * **Theme-based UI**: Use `@yoopta/themes-shadcn` (or a custom theme) for styling; the plugin provides a minimal fallback render. * **Shortcuts**: Insert via slash command or shortcuts `toc`, `table of contents`, `contents`. ## Element Props The TOC element supports these props (defaults are applied by the plugin): Maximum heading level to show: 1 = H1 only, 2 = H1 and H2, 3 = H1, H2, and H3. Optional title displayed above the list (e.g. "Contents", "On this page"). Block types to treat as headings. Must match plugin types registered in the editor (e.g. from `@yoopta/headings`). When true, render the list as a numbered list (1. 2. 3.). When true, the theme can show a toggle to collapse/expand the list (e.g. in the shadcn theme). ## Commands Use the plugin commands to insert and update the TOC block: ```typescript theme={null} import TableOfContents, { TableOfContentsCommands } from '@yoopta/table-of-contents'; // Insert a TOC block at the current position TableOfContentsCommands.insertTableOfContents(editor, { focus: true }); // Insert with custom props TableOfContentsCommands.insertTableOfContents(editor, { at: 0, depth: 2, title: 'On this page', showNumbers: true, collapsible: true, }); // Update an existing TOC block TableOfContentsCommands.updateTableOfContents(editor, blockId, { depth: 2, title: 'Contents', }); // Delete a TOC block TableOfContentsCommands.deleteTableOfContents(editor, blockId); // Build element only (e.g. for custom insert logic) const element = TableOfContentsCommands.buildTableOfContentsElements(editor, { depth: 3, title: 'Table of Contents', }); ``` ## Hook: useTableOfContentsItems The plugin exports a hook that returns the list of TOC items and subscribes to editor changes so the list stays up to date. Use it in your theme or custom render: ```typescript theme={null} import { useTableOfContentsItems } from '@yoopta/table-of-contents'; function MyTOCRender(props) { const editor = useYooptaEditor(); const { blockId, element } = props; const { depth = 3, headingTypes = ['HeadingOne', 'HeadingTwo', 'HeadingThree'] } = element.props ?? {}; const items = useTableOfContentsItems(editor, blockId, { depth, headingTypes }); return ( ); } ``` **Returned item shape:** * `id` — block id of the heading * `text` — plain text of the heading (no HTML) * `level` — 1, 2, or 3 for H1, H2, H3 ## Theme (Shadcn) Apply the shadcn theme so the TOC block uses Card, ScrollArea, and collapsible UI: ```jsx theme={null} import { applyTheme } from '@yoopta/themes-shadcn'; import TableOfContents from '@yoopta/table-of-contents'; const plugins = applyTheme([ TableOfContents, // ... Headings, Paragraph, etc. ]); ``` With the theme you get: * Card layout with optional title * Scrollable list (max height) for long TOCs * Click-to-scroll: focuses the heading block and scrolls it into view * Collapsible list when `collapsible: true` ## Parsers ### HTML * **Deserialize**: Recognizes `