Shadcn Editor

repository·main·Indexed 23 days ago

https://github.com/htmujahid/shadcn-editor

A highly extensible rich-text editor built on the Lexical framework and designed for seamless integration with Shadcn UI components. It features a modular architecture with support for custom nodes (EmojiNode, KeywordNode, AutocompleteNode), layout containers via INSERT_LAYOUT_COMMAND, and an AutoEmbedPlugin for services like YouTube and Twitter. The editor includes a BlockViewerProvider to manage the visibility of toolbar, footer, and plugin UI elements.

Tokens
4.9K
Snippets
8
Records
33
Agent score
80%

What's inside shadcn-editor

  1. Install Shadcn Editor via shadcn CLI

    main

    To add the Shadcn Editor to your project, use the shadcn CLI to add the @shadcn-editor/editor-x package. This will integrate the editor components into your existing shadcn/ui setup.

    npx shadcn@latest add @shadcn-editor/editor-x
  2. Extend the editor via the Feature Registry

    main

    The editor's extensibility is managed through a FEATURE_REGISTRY. To add new functionality (like a new toolbar item, block, or plugin), you must add an entry to the FEATURE_REGISTRY object.

    Each entry uses a unique key following the pattern category.itemKey (e.g., toolbarItems.undoRedo). This key is matched against the state shape provided by useBlockViewer() to determine which features to load.

    Each registry entry defines a FeatureSpec which maps the condition to the required code artifacts: imports, Lexical extensions, nodes, and JSX plugin components.

  3. How BlockViewerProvider manages editor UI state

    main

    The BlockViewerProvider acts as a central state manager for the editor's interface components. It maintains the visibility state for six distinct UI groups:

    1. Toolbar Items: Controls standard editing tools (e.g., font settings, undo/redo).
    2. Footer Items: Controls status and utility items (e.g., character count, export/import).
    3. Plugin Items: Controls extended functionality (e.g., emoji picker, mentions).
    4. Block Format Items: Controls text/block styling (e.g., headings, lists).
    5. Block Insert Items: Controls structural insertions (e.g., images, tables).
    6. Component Picker Items: Controls the items available in the component selection menu.

    Each group is managed via a Record<KeyType, boolean> where true means the item is visible. You can modify these states using the provided toggle... methods.

  4. Register the LayoutPlugin

    main

    The LayoutPlugin must be included in your Lexical editor configuration. It handles layout insertion, updates, and keyboard navigation (arrow keys) to ensure content can be added around layout blocks.

    Requirement: For the plugin to function, LayoutContainerNode and LayoutItemNode must be registered in the editor's node configuration. If they are missing, the plugin will throw an error during initialization.

  5. Implement the Shadcn Editor component

    main

    The Editor component is the primary entry point for using the shadcn-editor. It is built on top of Lexical and uses a LexicalExtensionComposer to manage a complex set of extensions, nodes, and plugins.

    To use the editor, import the Editor component and provide optional props for managing state:

    • editorState: An initial EditorState object.
    • editorSerializedState: An initial SerializedEditorState object (JSON format).
    • onChange: A callback function triggered when the EditorState changes.
    • onSerializedChange: A callback function triggered when the serialized JSON state changes.

    Note that the editor uses a ContentEditable component internally and is wrapped in a TooltipProvider for UI consistency.

  6. Configure ESLint for Shadcn Editor

    main

    The project uses a flat configuration format for ESLint. It applies recommended configurations for JavaScript, TypeScript, React Hooks, and Vite React Refresh.

    Key behaviors include:

    • Global Ignores: The dist directory is ignored by default.
    • TypeScript Rules: Unused variables starting with an underscore (_) are permitted. The @typescript-eslint/ban-ts-comment rule is disabled.
    • Fast Refresh Exceptions: To support the pattern where components and utility values (like buttonVariants) are exported from the same file, the react-refresh/only-export-components rule is disabled for:
      • src/components/ui/**/*.{ts,tsx}
      • src/components/editor/**/*.{ts,tsx}
    import js from '@eslint/js'
    import globals from 'globals'
    import reactHooks from 'eslint-plugin-react-hooks'
    import reactRefresh from 'eslint-plugin-react-refresh'
    import tseslint from 'typescript-eslint'
    import { defineConfig, globalIgnores } from 'eslint/config'
    
    export default defineConfig([
      globalIgnores(['dist']),
      {
        files: ['**/*.{ts,tsx}'],
        extends: [
          js.configs.recommended,
          tseslint.configs.recommended,
          reactHooks.configs.flat.recommended,
          reactRefresh.configs.vite,
        ],
        languageOptions: {
          ecmaVersion: 2020,
          globals: globals.browser,
        },
        rules: {
          '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
          '@typescript-eslint/ban-ts-comment': 'off',
        },
      },
      {
        files: ['src/components/ui/**/*.{ts,tsx}'],
        rules: {
          'react-refresh/only-export-components': 'off',
        },
      },
      {
        files: ['src/components/editor/**/*.{ts,tsx}'],
        rules: {
          'react-refresh/only-export-components': 'off',
        },
      },
    ])
  7. Define a FeatureSpec for new editor features

    main

    A FeatureSpec object defines everything required to enable a specific editor feature. It ensures that only the necessary code is imported and instantiated based on the active features.

    • imports: An array of ImportSpec objects defining the source module and the specific members (named or type-only) to be imported.
    • extensions: A list of Lexical extension names or configuration strings (e.g., configExtension(...)).
    • nodes: A list of Lexical node class names required for the feature.
    • plugins: A mapping of PluginSlot keys to arrays of JSX strings representing the components to be injected into specific parts of the editor UI.
  8. Serialize and deserialize EmojiNodes

    main

    The EmojiNode supports JSON serialization for saving and loading editor content.

    • Serialization: exportJSON() produces a SerializedEmojiNode which includes the className and the standard SerializedTextNode properties.
    • Deserialization: importJSON(serializedNode) reconstructs the node from its JSON representation.

    The serialized format follows this structure:

    export type SerializedEmojiNode = Spread<
      {
        className: string;
      },
      SerializedTextNode
    >;
  9. Manage editor UI visibility with useBlockViewer

    main

    The useBlockViewer hook allows you to control the visibility of various editor UI elements, such as toolbar items, footer items, plugins, and block formatting options. It provides access to the current visibility state (as a record of booleans) and toggle functions for each category. This is useful for building custom settings panels or conditional UI components that react to the editor's configuration state.

    To use it, ensure your component tree is wrapped in a BlockViewerProvider.

  10. Update an existing layout using UPDATE_LAYOUT_COMMAND

    main

    To change the column structure of an existing layout container, dispatch the UPDATE_LAYOUT_COMMAND. The command accepts an object containing the new template string and the nodeKey of the existing LayoutContainerNode.

    If the new template has more columns than the current one, the plugin will append new LayoutItemNode children. If it has fewer, it will remove the trailing items to match the new structure.

  11. Use AutocompleteNode for session-specific text

    main

    The AutocompleteNode is a specialized TextNode used to manage autocomplete suggestions within the editor. It uses a unique uuid to ensure that autocomplete nodes are session-specific. This prevents autocomplete nodes from appearing in other user sessions when collaboration is enabled and ensures a maximum of one autocomplete node per session.

    When the node's uuid does not match the current session's UUID, the DOM element is hidden (display: none).