Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/tiptap-feature-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@dextinity/cms-admin": minor
"@dextinity/cms-api": minor
---

Replace the TipTap Rich Text Block's `supports` array with one option per feature

`createTipTapRichTextBlock` now takes a single root options object with one option per editor feature, similar to TipTap's `StarterKit` configuration. Feature-specific options move into a nested options object of the feature they belong to: `headingLevels` becomes `heading: { levels: [...] }` and the link block becomes `link: { block: LinkBlock }`.
Comment thread
nsams marked this conversation as resolved.
Outdated

Every feature is enabled by default (except `underline` and `link`) and is disabled by passing `false`, so a configuration only has to state what deviates from the defaults instead of repeating every supported feature.

**Example**

```ts
// Before
createTipTapRichTextBlock({
supports: ["bold", "italic", "strike", "sub", "sup", "heading", "ordered-list", "unordered-list"],
headingLevels: [2, 3],
link: LinkBlock,
});

// After
createTipTapRichTextBlock({
nonBreakingSpace: false,
softHyphen: false,
heading: { levels: [2, 3] },
link: { block: LinkBlock },
});
```

The features are named after their option: `bold`, `italic`, `underline`, `strike`, `sub`, `sup`, `heading`, `orderedList`, `unorderedList`, `nonBreakingSpace`, `softHyphen`, `link`, and `history` (Admin only). The document-level limits `maxTextBlocks` and `listLevelMax` are unchanged.
2 changes: 1 addition & 1 deletion demo/admin/src/common/blocks/TipTapRichTextBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FormattedMessage } from "react-intl";
import { LinkBlock } from "./LinkBlock";

export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
childBlocks: {
productPrice: { block: ProductPriceBlock, display: "inline" },
productTeaser: { block: ProductTeaserBlock, display: "block" },
Expand Down
2 changes: 1 addition & 1 deletion demo/api/src/common/blocks/tip-tap-rich-text.block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Heading1ToHeading2Migration } from "./tip-tap-rich-text/migrations/2-he

export const TipTapRichTextBlock = createTipTapRichTextBlock(
{
link: LinkBlock,
link: { block: LinkBlock },
childBlocks: {
productPrice: { block: ProductPriceBlock, display: "inline" },
productTeaser: { block: ProductTeaserBlock, display: "block" },
Expand Down
94 changes: 66 additions & 28 deletions docs/docs/2-core-concepts/2-blocks/tiptap-rich-text-block.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,21 @@ The TipTap Rich Text Block only replaces the Draft.js-based `RichTextBlock`. It

Most features of the Draft.js `RichTextBlock` have a direct equivalent in the TipTap Rich Text Block.

| Draft.js `RichTextBlock` | TipTap Rich Text Block |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `createRichTextBlock` | `createTipTapRichTextBlock` |
| `rte.supports` (`SupportedThings[]`) | `supports` (`TipTapSupports[]`) |
| `bold`, `italic`, `strikethrough`, `sub`, `sup` | `bold`, `italic`, `strike`, `sub`, `sup` |
| `header-one` … `header-six` | `heading` support + the [text-block-type select](#text-block-type-and-styling-selects) (Heading 1–6) |
| `ordered-list`, `unordered-list` | `ordered-list`, `unordered-list` |
| `history` | `history` (Admin only) |
| `link`, `links-remove` | pass a `link` block (automatically enables `link` support) |
| `non-breaking-space`, `soft-hyphen` | `non-breaking-space`, `soft-hyphen` |
| `rte.blocktypeMap` (custom block types) | [`textBlockStyles`](#text-block-type-and-styling-selects) + the styling select |
| `rte.customInlineStyles` | [`inlineStyles`](#text-block-type-and-styling-selects) + the inline style select |
| `rte.listLevelMax` | `listLevelMax` |
| `rte.maxBlocks` | `maxTextBlocks` |
| Site rendering with `redraft` + `Renderers` | Site rendering with `renderTipTapRichText` + `nodeMapping`/`markMapping` |
| Draft.js `RichTextBlock` | TipTap Rich Text Block |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `createRichTextBlock` | `createTipTapRichTextBlock` |
| `rte.supports` (`SupportedThings[]`) | one option per feature ([Features](#features)) |
| `bold`, `italic`, `strikethrough`, `sub`, `sup` | `bold`, `italic`, `strike`, `sub`, `sup` |
| `header-one` … `header-six` | `heading` + the [text-block-type select](#text-block-type-and-styling-selects) (Heading 1–6) |
| `ordered-list`, `unordered-list` | `orderedList`, `unorderedList` |
| `history` | `history` (Admin only) |
| `link`, `links-remove` | `link: { block: LinkBlock }` |
| `non-breaking-space`, `soft-hyphen` | `nonBreakingSpace`, `softHyphen` |
| `rte.blocktypeMap` (custom block types) | [`textBlockStyles`](#text-block-type-and-styling-selects) + the styling select |
| `rte.customInlineStyles` | [`inlineStyles`](#text-block-type-and-styling-selects) + the inline style select |
| `rte.listLevelMax` | `listLevelMax` |
| `rte.maxBlocks` | `maxTextBlocks` |
| Site rendering with `redraft` + `Renderers` | Site rendering with `renderTipTapRichText` + `nodeMapping`/`markMapping` |

### Setup

Expand All @@ -73,7 +73,7 @@ import { createTipTapRichTextBlock } from "@dextinity/cms-admin";

import { LinkBlock } from "./LinkBlock";

export const TipTapRichTextBlock = createTipTapRichTextBlock({ link: LinkBlock });
export const TipTapRichTextBlock = createTipTapRichTextBlock({ link: { block: LinkBlock } });
```

In the Admin, the style and placeholder options additionally carry rendering information (`label` and `element`) that is used to preview them in the editor — see [Text block type and styling selects](#text-block-type-and-styling-selects).
Expand All @@ -87,14 +87,14 @@ import { createTipTapRichTextBlock } from "@dextinity/cms-api";

import { LinkBlock } from "./link.block";

export const TipTapRichTextBlock = createTipTapRichTextBlock({ link: LinkBlock });
export const TipTapRichTextBlock = createTipTapRichTextBlock({ link: { block: LinkBlock } });
```

The factory accepts additional Block Options such as the block name, like the old one:

```ts title="tip-tap-rich-text.block.ts"
export const TipTapRichTextBlock = createTipTapRichTextBlock(
{ link: LinkBlock },
{ link: { block: LinkBlock } },
{ name: "TipTapRichText" },
);
```
Expand Down Expand Up @@ -152,13 +152,49 @@ The generated type `TipTapRichTextBlockData.tipTapContent` is `unknown` on the s

:::

### Features

Every editor feature has its own option in the root options object, similar to [TipTap's `StarterKit`](https://tiptap.dev/docs/editor/extensions/functionality/starterkit). Most features are enabled by default and are turned off by passing `false`:

| Option | Default | Available in |
| ------------------ | ------- | ------------ |
| `bold` | `true` | API + Admin |
| `italic` | `true` | API + Admin |
| `underline` | `false` | API + Admin |
| `strike` | `true` | API + Admin |
| `sub` | `true` | API + Admin |
| `sup` | `true` | API + Admin |
| `heading` | `true` | API + Admin |
| `orderedList` | `true` | API + Admin |
| `unorderedList` | `true` | API + Admin |
| `nonBreakingSpace` | `true` | API + Admin |
| `softHyphen` | `true` | API + Admin |
| `link` | `false` | API + Admin |
| `history` | `true` | Admin only |

Features that can be configured further take an options object instead of `true`: `heading` accepts the allowed `levels`, `link` takes the `block` used for links (passing it enables links).

```ts title="tip-tap-rich-text.block.ts"
export const TipTapRichTextBlock = createTipTapRichTextBlock({
// Turn a feature off
strike: false,
// Turn a feature on that is disabled by default
underline: true,
// Configure a feature
heading: { levels: [2, 3] },
link: { block: LinkBlock },
});
```

The limits on the document as a whole aren't tied to a single feature and stay at the root of the options object: `maxTextBlocks` limits the number of top-level text blocks and `listLevelMax` the nesting depth of lists.

### Migrating existing content

Existing content stored by the Draft.js `RichTextBlock` (`{ draftContent: { blocks, entityMap } }`) can be migrated in place. Enable the built-in migration with the `migrateFromDraftJs` option on the **API** factory:

```ts title="tip-tap-rich-text.block.ts"
export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
// highlight-next-line
migrateFromDraftJs: true,
});
Expand All @@ -174,7 +210,7 @@ The migration converts:
- `LINK` entities → TipTap `link` marks,
- ` ` / `­` → non-breaking-space / soft-hyphen nodes.

It uses the block's `supports`, `textBlockStyles`, `link`, and `maxTextBlocks` options to build the target schema and validates the result. The conversion is **best effort**: if validation fails, it falls back to a stripped-down plain-text document in production (logging a warning) and throws in development so you can catch problems early.
It uses the block's enabled features and its `textBlockStyles` and `maxTextBlocks` options to build the target schema and validates the result. The conversion is **best effort**: if validation fails, it falls back to a stripped-down plain-text document in production (logging a warning) and throws in development so you can catch problems early.

:::caution Test with production content

Expand All @@ -188,7 +224,7 @@ If the old block used custom block types (via `blocktypeMap`) or custom inline s

```ts title="tip-tap-rich-text.block.ts"
export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
textBlockStyles: [{ name: "paragraph200", appliesTo: ["paragraph"] }],
inlineStyles: [{ name: "highlight" }],
// highlight-start
Expand All @@ -209,7 +245,9 @@ export const TipTapRichTextBlock = createTipTapRichTextBlock({
textBlockStyles: [{ name: "headline450", appliesTo: ["heading-2"] }],
migrateFromDraftJs: {
// highlight-next-line
textBlockStyleMap: { headline450: { textBlockType: "heading-2", textBlockStyle: "headline450" } },
textBlockStyleMap: {
headline450: { textBlockType: "heading-2", textBlockStyle: "headline450" },
},
},
});
```
Expand All @@ -225,7 +263,7 @@ import { createTipTapRichTextBlock, typeSafeBlockMigrationPipe } from "@dextinit

export const TipTapRichTextBlock = createTipTapRichTextBlock(
{
link: LinkBlock,
link: { block: LinkBlock },
migrateFromDraftJs: true,
},
{
Expand All @@ -247,7 +285,7 @@ Beyond replacing the old block, the TipTap Rich Text Block adds capabilities the

The editor toolbar offers two dropdowns to structure and style text blocks:

1. **Text block type** — the semantic type of the current block: _Default_ (paragraph) or _Heading 1_ … _Heading 6_. Shown when `heading` is in `supports`.
1. **Text block type** — the semantic type of the current block: _Default_ (paragraph) or _Heading 1_ … _Heading 6_. Shown when the `heading` feature is enabled.
2. **Styling** — a named style applied to the current text block (`textBlockStyles`). This decouples semantics ("this is a paragraph") from appearance ("small paragraph"), so editors pick a type _and_ a style independently.

There is a matching **inline style** dropdown for `inlineStyles`, which applies named styles to a text selection.
Expand All @@ -256,7 +294,7 @@ Styles are configured on the API (`name` + optional `appliesTo`) and in the Admi

```ts title="tip-tap-rich-text.block.ts (API)"
export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
textBlockStyles: [
{ name: "paragraph300", appliesTo: ["paragraph"] },
{ name: "paragraph200", appliesTo: ["paragraph"] },
Expand All @@ -271,7 +309,7 @@ import type { HTMLAttributes } from "react";
import { FormattedMessage } from "react-intl";

export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
textBlockStyles: [
{
name: "paragraph300",
Expand Down Expand Up @@ -341,7 +379,7 @@ The same configuration is used on the API and in the Admin:
import { createTipTapRichTextBlock } from "@dextinity/cms-api";

export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
childBlocks: {
productPrice: { block: ProductPriceBlock, display: "inline" },
productTeaser: { block: ProductTeaserBlock, display: "block" },
Expand All @@ -353,7 +391,7 @@ export const TipTapRichTextBlock = createTipTapRichTextBlock({
import { createTipTapRichTextBlock } from "@dextinity/cms-admin";

export const TipTapRichTextBlock = createTipTapRichTextBlock({
link: LinkBlock,
link: { block: LinkBlock },
childBlocks: {
productPrice: { block: ProductPriceBlock, display: "inline" },
productTeaser: { block: ProductTeaserBlock, display: "block" },
Expand Down
Loading
Loading