Skip to content

Custom Editor Components

A custom editor component allows you to create reusable, complex block-level component available in the rich text editor. Registered components appear under the Insert button on the editor toolbar. When clicked, they insert a predefined template into the editor at the current cursor position.

Overview

To register a custom editor component, use the registerEditorComponent method on the CMS object:

js
CMS.registerEditorComponent(definition);

The component definition object includes the following properties:

Required Properties

  • id (string): A unique identifier for the component. This is the name you will use to reference this component in the editor_components option for a RichText or Markdown field. It should be unique and not conflict with built-in component IDs (code-block, image).
  • fields (array of field definitions): An array defining the fields to be displayed in the component.
  • pattern (RegExp): A regular expression used to identify existing instances of the component in the Markdown content.
    • It’s recommended to use named capture groups corresponding to the field names so that fromBlock can be omitted if no additional processing is needed.
    • Matching could be either block (multiline) or inline, depending on the component. To match block content, use the s (dotAll) or m (multiline) flag, or include [\s\S] in the pattern.
  • fromBlock (function): A function that takes a regex match array and returns an object mapping field names to their values.
    • This property can be omitted if the pattern regular expression contains named capture groups corresponding to the field names, and no additional processing like type conversion is needed.
    • Otherwise, this property is required. You must provide a function to extract field values from the regex match.
  • toBlock (function): A function that takes an object mapping field names to their values and returns a string representing the Markdown content to be inserted.

Optional Properties

  • label (string): The text label displayed on the toolbar button. Defaults to the id value.
  • icon (string): A Material Symbols icon name to display on the toolbar button.
  • toPreview (function): A function that takes an object mapping field names to their values and returns a string or React class component representing the HTML preview of the component in the editor. If omitted, no preview is shown.
  • collapsed (boolean): If true, the component’s fields panel is collapsed by default when the component is inserted.

Unimplemented

toPreview is not yet supported in Sveltia CMS. It will be supported soon.

Using Components

Once registered, custom editor components can be used in any RichText or Markdown field. By default, all built-in and custom components are included. You can restrict which components are available by adding their id to the field’s editor_components array in the collection configuration.

For example, to allow only the built-in image component and custom callout and youtube components:

yaml
fields:
  - name: content
    label: Content
    widget: richtext
    editor_components: [image, callout, youtube]
toml
[[fields]]
name = "content"
label = "Content"
widget = "richtext"
editor_components = ["image", "callout", "youtube"]
json
{
  "fields": [
    {
      "name": "content",
      "label": "Content",
      "widget": "richtext",
      "editor_components": ["image", "callout", "youtube"]
    }
  ]
}
js
{
  fields: [
    {
      name: "content",
      label: "Content",
      widget: "richtext",
      editor_components: ["image", "callout", "youtube"],
    },
  ],
}

Examples

Callout

The following example demonstrates how to register a custom editor component for a "Callout" block:

js
CMS.registerEditorComponent({
  id: 'callout',
  label: 'Callout',
  icon: 'campaign',
  fields: [
    { name: 'type', label: 'Type', widget: 'select', options: ['info', 'warning', 'error'] },
    { name: 'message', label: 'Message' },
  ],
  pattern: /^:::callout (\w+)\n([\s\S]+?)\n:::/m,
  fromBlock: (match) => ({
    type: match[1],
    message: match[2].trim(),
  }),
  toBlock: (data) => `:::callout ${data.type}\n${data.message}\n:::`,
  toPreview: (data) => `:::callout ${data.type}\n${data.message}\n:::`,
});

In this example, the “Callout” component allows users to insert a callout block with a specified type (info, warning, or error) and a message. The pattern regular expression is used to identify existing callout blocks in the Markdown content, while the fromBlock and toBlock functions handle the conversion between the component's data and its Markdown representation.

This example demonstrates how to create a custom editor component for inserting a file link using the built-in file field type:

js
CMS.registerEditorComponent({
  id: 'file-link',
  label: 'File Link',
  icon: 'attach_file',
  fields: [
    { name: 'file', label: 'File', widget: 'file' },
    { name: 'text', label: 'Text to Display', default: '{{file}}' },
  ],
  pattern: /<a href="([^"]+?)" data-file-link>([^\n]+?)<\/a>/,
  fromBlock: (match) => ({
    file: decodeURI(match[1]),
    text: match[2],
  }),
  toBlock: (data) => `<a href="${data.file}" data-file-link>${data.text}</a>`,
  toPreview: (data) => `<a href="${data.file}" data-file-link>${data.text}</a>`,
});

Collapsible Note

Here’s an example of a collapsible “Note” component:

js
CMS.registerEditorComponent({
  id: 'note',
  label: 'Note',
  icon: 'note_alt',
  fields: [
    { name: 'summary', label: 'Summary' },
    { name: 'content', label: 'Content', widget: 'richtext' },
  ],
  pattern: /^<details>\s*<summary>(?<summary>.+?)<\/summary>\s*(?<content>[\s\S]+?)\s*<\/details>/m,
  toBlock: ({ summary, content }) =>
    `<details>\n<summary>${summary}</summary>\n${content}\n</details>`,
  toPreview: ({ summary, content }) =>
    `<details>\n<summary>${summary}</summary>\n<p>${content}</p>\n</details>`,
});

In this example, the “Note” component creates a collapsible section using HTML <details> and <summary> tags. The fromBlock function is omitted because the pattern regular expression uses named capture groups that correspond to the field names. The toBlock function generates the appropriate HTML structure for the note component based on the provided summary and content.

YouTube Embed with Hugo Shortcode

js
CMS.registerEditorComponent({
  id: 'youtube',
  label: 'YouTube',
  icon: 'youtube_activity',
  fields: [
    { name: 'id', label: 'ID' },
    { name: 'width', label: 'Width', widget: 'number', valueType: 'int', default: 560 },
    { name: 'height', label: 'Height', widget: 'number', valueType: 'int', default: 315 },
  ],
  pattern: /{{< youtube id="(?<id>.*?)"(?: width="(?<width>.*?)" height="(?<height>.*?)")? >}}/m,
  fromBlock: ({ groups: { id, width, height } = {} }) => ({
    id,
    width: width ? Number(width) : 560,
    height: height ? Number(height) : 315,
  }),
  toBlock: ({ id, width = 560, height = 315 }) =>
    `{{< youtube id="${id}" width="${width}" height="${height}" >}}`,
  toPreview: ({ id, width = 560, height = 315 }) =>
    id
      ? `<iframe src="https://www.youtube-nocookie.com/embed/${id}"
          width="${width}" height="${height}" allowfullscreen
          allow="autoplay; encrypted-media; picture-in-picture"></iframe>`
      : '',
});

In this example, the “YouTube” component allows users to embed YouTube videos using a Hugo shortcode. The pattern regular expression captures the video ID, width, and height from the shortcode. The fromBlock function processes the captured values, casting width and height to numbers. The toBlock function generates the shortcode string, while the toPreview function creates an iframe preview of the embedded video.

The pattern uses the m (multiline) flag to make the component block-level, though it’s not multiline in this case.

Using React for Preview

Released under the MIT License.