payload-plugin-lexical

repository·develop·Indexed 42 days ago

https://github.com/alessiogr/payload-plugin-lexical

A Lexical-based rich text editor plugin for Payload CMS 1.x (minimum version 1.6.32). It replaces the default Slate editor and provides advanced features including tables, AI suggestions via OpenAI, media embeds (YouTube, Twitter, Figma), LaTeX equations, and customizable editor configurations through the editorConfigModifier.

Tokens
36.9K
Snippets
133
Records
154
Agent score
52%

What's inside payload-plugin-lexical

  1. Customize Lexical editor features and configuration

    develop

    You can deeply customize the editor using the editorConfigModifier option within lexicalRichTextField. This allows you to:

    1. Modify Toggles: Enable or disable specific toolbar elements like textColor, fontSize, or tables via defaultEditorConfig.toggles.
    2. Manage Features: Replace or augment the default feature set by defining the defaultEditorConfig.features array. Each feature is a function that returns a Feature object.
    3. Add Custom Features: Push your own custom feature implementations into the features array.

    Note: A feature can consist of nodes, plugins, modals, and toolbar elements.

    import { CollectionConfig } from 'payload/types';
    import {
      lexicalRichTextField,
      EquationsFeature,
      EmojisFeature,
      EmojiPickerFeature,
      HorizontalRuleFeature,
      FigmaFeature,
      YouTubeFeature,
      TwitterFeature,
      SpeechToTextFeature,
      ImportFeature,
      ExportFeature,
      ClearEditorFeature,
      ReadOnlyModeFeature,
      ConvertFromMarkdownFeature,
      MentionsFeature,
      TreeViewFeature,
      KeywordsFeature,
      AutoCompleteFeature,
      CollapsibleFeature,
      TypingPerfFeature,
      PasteLogFeature,
      TestRecorderFeature,
      LinkFeature,
      TableOfContentsFeature,
      AISuggestFeature,
    } from 'payload-plugin-lexical';
    
    const Lexical: CollectionConfig = {
      slug: 'lexicalRichText',
      admin: {
        useAsTitle: 'title',
      },
      fields: [
        {
          name: 'title',
          type: 'text',
          required: true,
        },
        lexicalRichTextField({
          name: 'lexicalRichTextEditor',
          label: 'cool richtext editor',
          localized: true,
          editorConfigModifier: (defaultEditorConfig) => {
            defaultEditorConfig.debug = false;
            defaultEditorConfig.toggles.textColor.enabled = false;
            defaultEditorConfig.toggles.textBackground.enabled = false;
            defaultEditorConfig.toggles.fontSize.enabled = false;
            defaultEditorConfig.toggles.font.enabled = false;
            defaultEditorConfig.toggles.align.enabled = false;
            defaultEditorConfig.toggles.tables.enabled = true;
            defaultEditorConfig.toggles.tables.display = false;
    
            defaultEditorConfig.features = [
              EquationsFeature({}),
              EmojisFeature({}),
              EmojiPickerFeature({}),
              HorizontalRuleFeature({}),
              FigmaFeature({}),
              YouTubeFeature({}),
              TwitterFeature({}),
              SpeechToTextFeature({}),
              ImportFeature({}),
              ExportFeature({}),
              ClearEditorFeature({}),
              ReadOnlyModeFeature({}),
              ConvertFromMarkdownFeature({}),
              MentionsFeature({}),
              TreeViewFeature({ enabled: defaultEditorConfig.debug }),
              KeywordsFeature({}),
              AutoCompleteFeature({}),
              CollapsibleFeature({}),
              TypingPerfFeature({ enabled: defaultEditorConfig.debug }),
              PasteLogFeature({ enabled: defaultEditorConfig.debug }),
              TestRecorderFeature({ enabled: defaultEditorConfig.debug }),
              LinkFeature({}),
              TableOfContentsFeature({ enabled: false }),
              AISuggestFeature({}),
            ];
    
            // Add your own custom feature
            defaultEditorConfig.features.push(YourOwnCustomFeature({}));
    
            return defaultEditorConfig;
          },
        }),
      ],
    };
    
    export default Lexical;
  2. Install payload-plugin-lexical

    develop

    Install the plugin using yarn. This plugin requires a minimum Payload version of 1.6.32 and is specifically designed for Payload 1.x. It is not supported for Payload 2.x (though it serves as a reference for the new Lexical core in 2.0).

    yarn add payload-plugin-lexical
  3. Customize the Floating Text Format Toolbar

    develop

    You can extend the floating toolbar by adding custom components via the editorConfig. The plugin iterates over editorConfig.features and looks for floatingTextFormatToolbar.components. Each component in this array is a function that receives the editor and editorConfig as arguments.

    Additionally, if editorConfig.toggles.comments.enabled is true, a comment insertion button will be automatically included in the toolbar.

    // Example of how custom components are integrated via editorConfig
    // (Conceptual structure based on plugin implementation)
    const editorConfig = {
      features: [
        {
          floatingTextFormatToolbar: {
            components: [
              (editor, config) => (
                <button onClick={() => editor.dispatchCommand(MY_COMMAND, null)}>
                  Custom Action
                </button>
              ),
            ],
          },
        },
      ],
      toggles: {
        comments: { enabled: true }
      }
    };
  4. How AutocompletePlugin works

    develop

    The AutocompletePlugin operates through a lifecycle of detection, querying, and resolution:

    1. Detection: The plugin uses an UpdateListener to monitor editor changes. It uses an internal $search function to check if the current selection is a collapsed range at the end of a text node.
    2. Querying: If a word fragment is detected, it triggers a query (which returns a SearchPromise). This promise contains the actual Promise<null | string> and a dismiss() method to cancel outdated requests.
    3. Suggestion Insertion: If a match is found, the plugin inserts an AutocompleteNode into the editor at the current selection point. This node acts as a placeholder for the suggestion.
    4. Resolution:
      • Acceptance: If the user triggers the KEY_TAB_COMMAND or KEY_ARROW_RIGHT_COMMAND, the plugin replaces the AutocompleteNode with a TextNode containing the completed word.
      • Dismissal: If the user continues typing or the selection changes such that the match is no longer valid, the plugin calls $clearSuggestion() to remove the AutocompleteNode and clear the suggestion state.
  5. Configure custom modals via editorConfig

    develop

    The ModalPlugin allows for extending the editor with custom modals by defining them in the editorConfig. Within the features array of the editorConfig, you can provide modal configurations that the plugin will automatically render and manage.

    Each custom modal in the features array should follow this structure:

    • modals: An array of modal objects.
    • openModalCommand.type: A string that matches the value passed to OPEN_MODAL_COMMAND to trigger this specific modal.
    • openModalCommand.command: A function that receives (toggleModal, editDepth, uuid) and is responsible for opening the modal.
    • modal: A function that returns a React component, receiving editorConfig as an argument.
    // Conceptual structure of a feature with a custom modal
    const feature = {
      modals: [
        {
          openModalCommand: {
            type: 'my-custom-modal',
            command: (toggleModal, editDepth, uuid) => {
              // logic to open modal
            }
          },
          modal: ({ editorConfig }) => <MyCustomModalComponent />
        }
      ]
    };
  6. Manage cell editor context with TableContext and CellContext

    develop

    The plugin uses a React Context pattern to allow components to configure and access the settings for editors used within table cells.

    • TableContext: A provider component that must wrap the area where cell configuration is managed. It initializes the CellContext state.
    • CellContext: The actual context object. It provides cellEditorConfig, cellEditorPlugins, and a set function to update them.

    This pattern is essential if you are building custom cell components that need to know which nodes or plugins should be active when a user clicks into a table cell.

    import { TableContext } from './path-to-plugin';
    
    function MyEditor() {
      return (
        <TableContext>
          {/* Your Lexical Editor and TablePlugin here */}
        </TableContext>
      );
    }
  7. How the Mentions plugin handles triggers and matching

    develop

    The plugin uses a multi-strategy approach to identify when a user is attempting to create a mention:

    1. At-Sign Mentions: Matches strings starting with @ followed by valid characters (non-space, non-punctuation). It supports a length limit of 75 characters for the mention string.
    2. Capitalized Name Mentions: If no @ match is found, it looks for sequences of capitalized words (e.g., Jane Doe). This requires a minimum match length of 3 characters.
    3. Slash Command Exclusion: The plugin uses useBasicTypeaheadTriggerMatch for /. If a slash match is detected, the mention matching logic is aborted for that segment to prevent overlapping triggers.

    Matching logic is encapsulated in getPossibleQueryMatch, which prioritizes @ triggers over capitalized names.

  8. How the Floating Link Editor works

    develop

    The floating link editor follows a specific lifecycle and interaction model:

    1. Detection: The plugin listens to SELECTION_CHANGE_COMMAND and editor updates. It uses $findMatchingParent to check if the current selection is contained within a $isLinkNode or an $isAutoLinkNode.
    2. UI Rendering: If a link is detected, the LinkEditor component is rendered via a React Portal into the anchorElem.
    3. Link Editing: Clicking the edit button in the floating UI opens a LinkDrawer. This drawer uses Payload's form logic to allow users to choose between 'custom' URLs or 'internal' documents (relations).
    4. Command Dispatch: When the drawer is submitted, the plugin calculates the new link attributes and dispatches the TOGGLE_LINK_COMMAND with the updated data.
    5. Deletion: Clicking the trash icon dispatches TOGGLE_LINK_COMMAND with null, which removes the link node.
    6. Positioning: The plugin dynamically calculates the position of the floating element based on the bounding client rect of the selected text's parent element.
  9. How AISuggestPlugin works

    develop

    The AISuggestPlugin implements an asynchronous autocomplete lifecycle:

    1. Detection: The plugin listens to editor updates. It uses an internal $search function to check if the current selection is a collapsed range and if the surrounding text is long enough (at least 5 characters) to warrant a suggestion.
    2. Querying: It invokes a query function (which by default uses an AutocompleteServer implementation) to fetch suggestions based on the detected text.
    3. Display: If a suggestion is returned, the plugin inserts an AISuggestNode into the editor at the current selection point. This node acts as a placeholder for the suggested text.
    4. Acceptance: The user can accept the suggestion using KEY_TAB_COMMAND, KEY_ARROW_RIGHT_COMMAND, or a swipe-right gesture. Upon acceptance, the AISuggestNode is replaced by a standard text node containing the suggestion text.
    5. Dismissal: Suggestions are cleared if the selection changes significantly, if a new search replaces the current one, or if the component unmounts.
  10. Create a custom Lexical Feature

    develop

    A Feature is a configuration object used to extend the Lexical editor within Payload. It allows you to register new plugins, custom nodes, modals, and toolbar items.

    To implement a feature, you return an object containing:

    • plugins: An array of objects containing React components that act as Lexical plugins.
    • nodes: An array of Lexical node classes to be registered in the editor.
    • modals: An array of modal configurations, including the component to render and a openModalCommand to trigger it.
    • toolbar: An object containing insert items, which are functions that return React components (like DropDownItem) to be displayed in the editor's insertion menu.
    import { LexicalEditor } from 'lexical';
    import { Feature, EditorConfig } from '../../../../src/types';
    import { DropDownItem } from '../../../../src/fields/LexicalRichText/ui/DropDown';
    import { OPEN_MODAL_COMMAND } from '../../../../src/fields/LexicalRichText/plugins/ModalPlugin';
    
    export function PayloadBlockFeature(props: {}): Feature {
      return {
        plugins: [
          {
            component: <PayloadBlockPlugin key="payloadBlock" />,
          },
        ],
        nodes: [PayloadBlockNode],
        modals: [
          {
            modal: InsertPayloadBlockDialog,
            openModalCommand: {
              type: 'payloadBlock',
              command: (toggleModal, editDepth, uuid) => {
                // Logic to open the modal
                toggleModal(payloadBlockDrawerSlug);
              },
            },
          },
        ],
        toolbar: {
          insert: [
            (editor: LexicalEditor, editorConfig: EditorConfig) => {
              return (
                <DropDownItem
                  key="payloadBlock"
                  onClick={() => {
                    editor.dispatchCommand(OPEN_MODAL_COMMAND, 'payloadBlock');
                  }}
                  className="item"
                >
                  <i className="icon block" />
                  <span className="text">Payload Block</span>
                </DropDownItem>
              );
            },
          ],
        },
      };
    }
  11. Use the CommentPlugin for rich text commenting

    develop

    The CommentPlugin is a React component that enables inline commenting and threaded discussions within a Lexical editor. It manages the UI for adding comments, viewing a comments panel, and handling the relationship between comments and the text via MarkNodes.

    Key features include:

    • Inline Comments: Users can select text and trigger a comment input box.
    • Threaded Discussions: Supports replies to existing comments.
    • Comments Panel: A toggleable sidebar/overlay that lists all comments and threads.
    • Visual Highlighting: Automatically highlights text associated with active comments.

    To use it, wrap your Lexical editor content with CommentsContext to provide the underlying CommentStore, and then include the CommentPlugin within the LexicalComposer context.

    // 1. Wrap your editor area with CommentsContext
    <CommentsContext initialComments={yourInitialComments}>
      <LexicalComposer initialConfig={config}>
        <CommentPlugin />
        {/* ... other plugins and editor components ... */}
      </LexicalComposer>
    </CommentsContext>