# Rich text editor

> A text field with a formatting toolbar for writing headings, lists and styled text, built on Tiptap.

## Anatomy

A `.rich-text-editor` turns a `<textarea>` into an editor with a formatting toolbar. It is built on [Tiptap](https://tiptap.dev/), which the page supplies.

Put a docked [toolbar](/toolbars.html.md) and a `<textarea>` in the frame. The textarea's value is the starting HTML. The component hides the textarea, mounts the editor in a `.rich-text-editor-content` after it, and writes the editor's HTML back on every change. Without JavaScript, the textarea stays visible and the toolbar is hidden.

Release notes

<h2>Version 2.4</h2><p>This release adds <strong>offline mode</strong> and fixes two sync bugs.</p><ul><li><p>Drafts save while offline.</p></li><li><p>Edits from two devices no longer conflict.</p></li></ul>

```
<label for="body">Release notes</label>
<div class="rich-text-editor">
  <div class="toolbar docked">
    <button type="button" class="circle" data-command="toggleBold" aria-label="Bold">
      <span class="material-symbols" aria-hidden="true">format_bold</span>
    </button>
    <button type="button" class="circle" data-command="toggleHeading"
            data-attributes='{"level":2}' aria-label="Heading">
      <span class="material-symbols" aria-hidden="true">format_h2</span>
    </button>
    <span class="max"></span>
    <button type="button" class="circle" data-command="undo" aria-label="Undo">
      <span class="material-symbols" aria-hidden="true">undo</span>
    </button>
  </div>
  <textarea id="body" name="body"><p>Starting content</p></textarea>
</div>
```

The editable area is a multi-line `textbox`. It takes its name from the textarea's `<label>`, `aria-label` or `aria-labelledby`. Clicking the label focuses it. Tiptap's keyboard shortcuts work, such as Ctrl+B for bold and Ctrl+Z to undo. Tab moves to the toolbar buttons.

## Setup

ExpressiveCSS does not bundle Tiptap, and `AutoInit()` does not start the editor. To set it up:

1.  Install Tiptap.
2.  Pass its `Editor` class and your extensions to `RichTextEditor.init()`.

```
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
```

```
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@expressivecss/expressive';

RichTextEditor.init(document.querySelector('.rich-text-editor'), {
  Editor,
  extensions: [StarterKit]
});
```

## Commands

A button's `data-command` names a Tiptap command, and `data-attributes` holds its argument as JSON. A click runs the command on the selection and returns focus to the editor. Any command your extensions add works the same way.

A button is disabled while its command cannot run, for example Undo with nothing to undo. Buttons for `toggle` commands also show whether the selection is formatted. `toggleBold` sets `aria-pressed` and `.active` while the selection is bold. `toggleHeading` with `{"level":2}` sets them while the selection is in a second-level heading.

## Forms

The textarea submits the editor's HTML with its form and fires `input` on every change. An empty editor submits an empty value. Tiptap drops markup its extensions do not know. The value still comes from the browser, so sanitize it on the server before you store or show it. A `disabled` or `readonly` textarea makes the editor read-only and disables the toolbar.

## Options

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `Editor` | Class | `null` | Tiptap's `Editor` class. Required. |
| `extensions` | Array | `[]` | Tiptap extensions, usually `[StarterKit]`. |

## Methods

```
const instance = Expressive.RichTextEditor.getInstance(document.querySelector('.rich-text-editor'));
instance.editor.commands.setContent('<p>Replaced</p>'); // the Tiptap editor
instance.destroy(); // removes the editor and shows the textarea again
```

## Tokens

Override these on the `.rich-text-editor`.

| Token | Default |
| --- | --- |
| `--md-comp-rich-text-editor-min-height` | 160px |
| `--md-comp-rich-text-editor-shape` | 16px |
