diff --git a/content/guides/01.data-model/3.interfaces.md b/content/guides/01.data-model/3.interfaces.md index a32f7d9e..dc17218c 100644 --- a/content/guides/01.data-model/3.interfaces.md +++ b/content/guides/01.data-model/3.interfaces.md @@ -96,13 +96,17 @@ The What You See Is What You Get (WYSIWYG) editor provides a text area with rich | Folder | Default folder to store uploaded files. Does not affect existing files. | | Soft Limit | Used to limit the number of characters within the Data Studio. | | Static Access Token | Token appended to asset URLs when displaying in the editor. | -| Custom Formats | JSON array of custom formatting styles to add to the editor's formatting menu. Only inline formats are supported; block, selector, and wrapper formats are ignored. | +| Custom Formats | JSON array of custom formatting entries added to the editor's Formats menu. Supports inline, block, selector, and grouped entries. `wrapper` entries are not supported. See [Rich Text](/guides/data-model/rich-text#custom-formats). | | Options Override | Deprecated and no longer applied. See the note below. | ::callout{icon="i-lucide-info"} **The editor's engine changed in Directus 12.** The WYSIWYG interface is now built on [Tiptap](https://tiptap.dev). Existing fields keep working without migration, but stored HTML is normalized to the editor's supported markup on first edit, and the **Options Override** (`tinymceOverrides`) option is deprecated. See the [Version 12 breaking changes](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap) for details. :: +::callout{icon="i-lucide-book-open" color="primary" to="/guides/data-model/rich-text"} +Configure custom formats, understand what the editor stores, and style the output on your frontend in the [Rich Text](/guides/data-model/rich-text) guide. +:: + ### Markdown ![A markdown text editor with a toolbar with formatting options. Edit and preview tabs.](/img/b41d6822-35ab-48db-ada9-dd3a723a5c52.webp) diff --git a/content/guides/01.data-model/4.rich-text.md b/content/guides/01.data-model/4.rich-text.md new file mode 100644 index 00000000..7700b26e --- /dev/null +++ b/content/guides/01.data-model/4.rich-text.md @@ -0,0 +1,307 @@ +--- +stableId: 2947492c-9b05-4261-b4dd-28ba4b0ceaba +title: Rich Text +description: Configure custom formats for the WYSIWYG interface, understand what the editor stores, and style the resulting HTML in your app's frontend CSS. +--- + +The [WYSIWYG interface](/guides/data-model/interfaces#wysiwyg) stores its value as HTML. This page covers the **Custom Formats** option, which adds your own formatting entries to the editor. + +::callout{icon="i-lucide-info"} +The editor is built on [Tiptap](https://tiptap.dev) as of Directus 12. It stores only the markup its schema models. See [Version 12 breaking changes](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap) for the supported HTML and how existing values are normalized. +:: + +## Custom Formats + +**Custom Formats** takes a JSON array of formatting entries. Set it on the field's interface options. When the array is not empty, a **Formats** dropdown is added to the editor toolbar. There is no toolbar option to enable it. + +The format is a subset of TinyMCE's `style_formats`, so existing configurations often carry over unchanged. + +Every entry needs a `title` and one of the following keys: + +| Key | Applies to | +| ---------- | --------------------------------------------------------------------------------------------- | +| `inline` | A tag wrapped around the selected text, such as `span`. | +| `block` | A single block tag. Converts the block when its tag differs. See [Block formats](#block-formats). | +| `selector` | One or more comma-separated block tags. Never converts the block. | +| `items` | An array of entries, grouped into a submenu. See [Grouped formats](#grouped-formats). | + +An `inline`, `block`, or `selector` entry also needs at least one of `classes` or `attributes`, and can set both. The editor uses them to recognize the format again when the value is reloaded, so an entry carrying neither is skipped. An `items` group is a container and needs neither. + +`styles` is the only optional key. + +### Example + +```json +[ + { "title": "Lead paragraph", "block": "p", "classes": "lead" }, + { "title": "Subheading", "selector": "h2,h3", "classes": "subheading" }, + { + "title": "Callouts", + "items": [ + { "title": "Info", "block": "p", "classes": "callout callout-info" }, + { "title": "Warning", "block": "p", "classes": "callout callout-warning" } + ] + }, + { + "title": "Highlight", + "inline": "span", + "classes": "highlight", + "styles": { "background": "#ffff00" } + } +] +``` + +Applying **Lead paragraph** to a paragraph, then **Highlight** to the word `lead` inside it, turns: + +```html +

A lead paragraph.

+``` + +into: + +```html +

A lead paragraph.

+``` + +Define `lead`, `subheading`, `callout`, and `highlight` in your frontend's CSS. Directus stores the classes but does not style them. See [Styling the output on your frontend](#styling-the-output-on-your-frontend). + +### Inline formats + +An `inline` entry renders a tag around the selection, carrying its `classes`, its `attributes`, and its `styles` serialized into a `style` attribute: + +```json +{ + "title": "Highlight", + "inline": "span", + "classes": "highlight", + "styles": { "background": "#ffff00" }, + "attributes": { "title": "Highlighted text" } +} +``` + +Selecting the word `text` and applying the format turns: + +```html +

Some text.

+``` + +into: + +```html +

Some text.

+``` + +Inline entries are the only formats that write a `style` attribute into the content, so they are also the only ones with a visible effect inside the editor. The trade-off is an inline style in your stored HTML, which is harder to override from your frontend CSS. + +### Block formats + +A `block` entry applies to one block tag and takes a single tag only. Use `selector` for a list. + +```json +{ "title": "Lead paragraph", "block": "p", "classes": "lead" } +``` + +When the selected block's tag differs from the entry's tag, the block is converted. **Conversion works between paragraphs and headings only**, because those are the two block types that hold the same content. + +With `{ "title": "Subheading", "block": "h2", "classes": "subheading" }`, applying the format to a paragraph turns: + +```html +

Section title

+``` + +into: + +```html +

Section title

+``` + +A `block: 'p'` entry converts the other way, turning a heading back into a paragraph. + +Every other tag applies without converting. With `{ "title": "Pull quote", "block": "blockquote", "classes": "pull-quote" }`, applying the format to a block that is already a `
` turns: + +```html +
A quote.
+``` + +into: + +```html +
A quote.
+``` + +Applying it to a paragraph leaves the paragraph untouched, and the editor logs a warning in the browser console when it loads the format. Use `selector` when you never want conversion. + +### Selector formats + +A `selector` entry applies to the block tags you list and never changes the block's type: + +```json +{ "title": "Subheading", "selector": "h2,h3", "classes": "subheading" } +``` + +Applying it to an `

` or `

` adds the class, turning: + +```html +

Section title

+``` + +into: + +```html +

Section title

+``` + +Applying it to a paragraph does nothing, because `p` is not in the entry's list: + +```html +

Section title

+``` + +stays: + +```html +

Section title

+``` + +### Grouped formats + +An `items` array groups entries into a submenu in the **Formats** dropdown: + +```json +{ + "title": "Callouts", + "items": [ + { "title": "Info", "block": "p", "classes": "callout callout-info" }, + { "title": "Warning", "block": "p", "classes": "callout callout-warning" } + ] +} +``` + +Groups support one level. A group nested inside another group is skipped, and a group whose entries are all invalid is dropped along with them. + +### Which tags a block format can target + +`block` and `selector` accept only tags the editor models as a node: + +`p`, `h1` through `h6`, `pre`, `blockquote`, `section`, `article`, `figure`, `figcaption`, `details`, `summary`, `dl`, `dt`, `dd`, `hr`, `img`, `ul`, `ol`, `li`, `table`, `tr`, `td`, `th`, `video`, `audio`, `iframe`. + +`div` and `span` are not on the list, so they cannot carry a block format. A `block: 'div'` entry is skipped. This matters when you migrate a TinyMCE configuration, since TinyMCE formats commonly wrap content in a `div`. It is the same reason a stored `
` is unwrapped when the editor loads it. Use a `section`, `article`, or `figure` entry instead, or move the wrapper into your frontend template. + +A `selector` entry drops the tags the editor does not model and keeps the rest, so `selector: 'h2,div,p'` still applies to `

` and `

`. The entry is skipped only when none of its tags are modelled. + +`selector` takes plain tag names. Compound CSS selectors such as `p.lead`, `div > p`, and `#main` are skipped rather than silently matching nothing. + +### Invalid entries are skipped + +An entry the editor cannot build is skipped, and the rest of the array still loads. Every skipped entry logs a warning to the **browser console**. Nothing appears in the interface, so check the console when a format does not show up in the dropdown. + +Entries are skipped when they: + +- Have no `title`. +- Have none of `inline`, `block`, `selector`, or `items`. +- Have no `classes` and no `attributes`, unless the entry is an `items` group. +- Use `wrapper`, which is not supported. +- Name only tags the editor does not model, such as `block: 'div'` or `selector: 'div,span'`. +- Pass a compound CSS selector such as `p.lead` or `#main` instead of a plain tag name. +- Pass a comma-separated list to `block` instead of `selector`. +- Nest a group inside another group. + +## Gotchas + +### The editor does not load your frontend CSS + +::callout{icon="i-lucide-triangle-alert" color="warning"} +The editor is not a preview of your site. Classes applied through custom formats have no styling attached in the Data Studio. The exception is [inline formats](#inline-formats), which write a `style` attribute into the content and do render in the editor. +:: + +Directus loads no project stylesheet into the editing surface, so a class like `lead`, `float-left`, or `text-center` has no visual effect inside the WYSIWYG. The class is stored and round-trips correctly. It just has nothing styling it in the Data Studio. + +This surprises authors, who apply a format, see no change, and assume it is broken. Point them at the **Formats** dropdown instead: it shows the active format in its label and highlights the matching row. That is the confirmation that a format applied, not the rendered content. + +The editor's own content styles can also contradict your class. The editor styles `figure` as `display: table; margin: 0.8125rem auto`, so a captioned image with a `float-left` class renders centered in the editor and floated on your frontend. The same applies to anything depending on `display`, `float`, `width`, or a flex or grid context your frontend provides. + +There is currently no option to inject custom CSS into the editor. The deprecated **Options Override** (`tinymceOverrides`) option is no longer applied, so TinyMCE's `content_css` has no equivalent. + +Because of this, layout classes are the riskiest thing to hand to non-technical authors: no feedback in the editor, and a result that depends entirely on CSS the editor cannot see. Prefer semantic formats such as `lead`, `callout`, or `subheading` over layout ones where you can. + +### `styles` on a block entry only styles the dropdown + +On an `inline` entry, `styles` is written into the content as a `style` attribute. On a `block` or `selector` entry, `styles` only styles the entry's label inside the **Formats** dropdown. It is not written to the content. + +The editor does not round-trip `style` on block nodes. Only `class`, `id`, `title`, `role`, `lang`, `dir`, `data-*`, and `aria-*` survive a save and load cycle. TinyMCE applied block `styles` to the block itself, so this is a behavior change to account for when migrating. + +Use `classes` for block styling and define the CSS on your frontend. + +### Attributes on a block entry are filtered + +`attributes` on a `block` or `selector` entry accepts only `id`, `title`, `role`, `lang`, `dir`, `data-*`, and `aria-*`. Anything else, including `style` and `target`, is dropped with a console warning. + +`attributes: { "class": "…" }` is merged into `classes`, so toggling the format off removes those classes too. + +Inline entries do not filter `attributes`. + +## Toggling behavior + +Selecting an active format in the dropdown removes it. Applying and removing a block format behaves as follows: + +- Removing a format strips only the classes and attributes that format configured. Unrelated classes, `id`, `data-*`, and `aria-*` on the same block survive. +- The block keeps its tag. A `block: 'h2'` format does not turn the heading back into a paragraph when you remove it. +- A format applies to every eligible block in the selection as a single undo step. +- A format counts as active when every eligible block in the selection carries all of its classes, or all of its attributes for a format anchored on attributes alone. + +## Image captions + +Adding or editing an image opens a drawer with a **Caption** field. A non-empty caption wraps the image in a `

`: + +```html +
+ A wind turbine +
A wind turbine at dusk.
+
+``` + +Style `figure` and `figcaption` on your frontend to match. The editor centers them, which your own CSS will override. + +Behavior worth knowing: + +- Clearing the caption reverts to a bare ``, but only when the `
` holds nothing but images and carries no attributes of its own. A `
` you added deliberately is kept, and its caption is removed on its own. +- Editing an existing image updates it in place, so its attributes and its surrounding `
` survive the edit. +- Pressing `Enter` inside a caption leaves the figure and starts a new paragraph after it, rather than adding a second `
`. +- Pressing `Backspace` in an empty caption removes the caption. +- Deleting the image out of a figure removes the orphaned caption too. +- Stored `
` and `
` markup round-trips, including a caption placed before the image and a figure holding only a caption. + +## Styling the output on your frontend + +Directus stores the HTML. Rendering and styling it is your frontend's job. + +Add the classes your formats configure to your stylesheet: + +```css +.lead { + font-size: 1.25rem; + line-height: 1.6; +} + +.subheading { + color: #6644ff; + text-transform: uppercase; +} + +.callout { + border-left: 4px solid; + padding: 1rem; +} + +.callout-info { border-color: #3399ff; } +.callout-warning { border-color: #ffaa00; } +``` + +Two things to keep in mind: + +- Scope these styles so they do not collide with the rest of your site. Rendering the value inside a wrapper such as `.prose` and prefixing your selectors keeps author-applied classes from leaking. +- Review your selectors before upgrading to Directus 12 if you style stored HTML by tag, class, or attribute. Markup the editor does not model, such as `
` wrappers, is removed when a field is edited and saved. + +## Next Steps + +Read about the [WYSIWYG interface options](/guides/data-model/interfaces#wysiwyg), the [supported HTML and normalization behavior](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap), and [keyboard shortcuts](/getting-started/accessibility) for the editor. diff --git a/content/guides/01.data-model/4.relationships.md b/content/guides/01.data-model/5.relationships.md similarity index 100% rename from content/guides/01.data-model/4.relationships.md rename to content/guides/01.data-model/5.relationships.md