-
Notifications
You must be signed in to change notification settings - Fork 83
docs: document block-level custom formats and image captions for WYSIWYG #801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alvarosabu
wants to merge
3
commits into
main
Choose a base branch
from
docs/wysiwyg-block-custom-formats
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| <p>A lead paragraph.</p> | ||
| ``` | ||
|
|
||
| into: | ||
|
|
||
| ```html | ||
| <p class="lead">A <span class="highlight" style="background: #ffff00">lead</span> paragraph.</p> | ||
| ``` | ||
|
|
||
| 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 | ||
| <p>Some text.</p> | ||
| ``` | ||
|
|
||
| into: | ||
|
|
||
| ```html | ||
| <p>Some <span class="highlight" style="background: #ffff00" title="Highlighted text">text</span>.</p> | ||
| ``` | ||
|
Comment on lines
+84
to
+92
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, before and after |
||
|
|
||
| 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 | ||
| <p>Section title</p> | ||
| ``` | ||
|
|
||
| into: | ||
|
|
||
| ```html | ||
| <h2 class="subheading">Section title</h2> | ||
| ``` | ||
|
|
||
| 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 `<blockquote>` turns: | ||
|
|
||
| ```html | ||
| <blockquote>A quote.</blockquote> | ||
| ``` | ||
|
|
||
| into: | ||
|
|
||
| ```html | ||
| <blockquote class="pull-quote">A quote.</blockquote> | ||
| ``` | ||
|
|
||
| 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 `<h2>` or `<h3>` adds the class, turning: | ||
|
|
||
| ```html | ||
| <h3>Section title</h3> | ||
| ``` | ||
|
|
||
| into: | ||
|
|
||
| ```html | ||
| <h3 class="subheading">Section title</h3> | ||
| ``` | ||
|
|
||
| Applying it to a paragraph does nothing, because `p` is not in the entry's list: | ||
|
|
||
| ```html | ||
| <p>Section title</p> | ||
| ``` | ||
|
|
||
| stays: | ||
|
|
||
| ```html | ||
| <p>Section title</p> | ||
| ``` | ||
|
|
||
| ### 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 `<div class="float-left">` 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 `<h2>` and `<p>`. 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 `<figure>`: | ||
|
|
||
| ```html | ||
| <figure> | ||
| <img src="https://example.com/assets/2b1a…" alt="A wind turbine" /> | ||
| <figcaption>A wind turbine at dusk.</figcaption> | ||
| </figure> | ||
| ``` | ||
|
|
||
| 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 `<img>`, but only when the `<figure>` holds nothing but images and carries no attributes of its own. A `<figure class="…">` 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 `<figure>` survive the edit. | ||
| - Pressing `Enter` inside a caption leaves the figure and starts a new paragraph after it, rather than adding a second `<figcaption>`. | ||
| - Pressing `Backspace` in an empty caption removes the caption. | ||
| - Deleting the image out of a figure removes the orphaned caption too. | ||
| - Stored `<figure>` and `<figcaption>` 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 `<div>` 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. | ||
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe clearer if we give before and after here