diff --git a/next.config.mjs b/next.config.mjs index 800c8e1e6..099499259 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -62,6 +62,11 @@ const nextConfig = { destination: '/editor/getting-started/install', permanent: true, }, + { + source: '/editor/getting-started/install/cdn', + destination: '/editor/getting-started/install/vanilla-javascript', + permanent: true, + }, { source: '/editor/markdown/getting-started', destination: '/editor/markdown/getting-started/installation', diff --git a/src/content/editor/core-concepts/decorations.mdx b/src/content/editor/core-concepts/decorations.mdx new file mode 100644 index 000000000..66542876b --- /dev/null +++ b/src/content/editor/core-concepts/decorations.mdx @@ -0,0 +1,167 @@ +--- +title: Decorations +description: How to use Tiptap's Decorations API - implementing decorations in extensions, decoration types, and performance tips. +meta: + title: Decorations | Tiptap Editor Docs + description: Learn how to implement and optimize decorations in Tiptap extensions. + category: Editor +--- + +## TL;DR + +Decorations let you draw styling or UI on top of the document without changing the document content. Add a `decorations()` factory to an extension and return a small array of simple decoration items. Use `shouldUpdate` to avoid unnecessary recalculation. + +## What is the Decoration API? + +The Decoration API is a small, extension-facing surface that lets extensions describe visual decorations (highlights, badges, widgets) without mutating the document. Tiptap turns those descriptions into ProseMirror decorations and renders them in the editor view. + +## Decoration types + +- inline – styling applied to a range of text (e.g., highlights). +- node – attributes applied to an entire node (e.g., add a class to a paragraph). +- widget – a DOM node rendered at a specific position (e.g., an inline badge or button). Widgets are non-editable. + +## How to add decorations to an extension + +Add a `decorations()` factory to your extension that returns an object with `create({ state, view, editor })` and an optional `shouldUpdate`. + +Inline example (using the helper): + +```js +import { Extension, createInlineDecoration } from '@tiptap/core' + +export const MakeWordRed = Extension.create({ + name: 'makeWordRed', + + decorations: () => ({ + create({ state }) { + // return an array of decoration items + return [createInlineDecoration(5, 11, { style: 'color: red' })] + }, + shouldUpdate: ({ tr }) => tr.docChanged, + }), +}) +``` + +Node example (using the helper): + +```js +import { Extension, createNodeDecoration } from '@tiptap/core' + +export const HighlightParagraph = Extension.create({ + name: 'highlightParagraph', + + decorations: () => ({ + create({ state }) { + // highlight the first paragraph + const pos = 0 + const end = 50 + return [createNodeDecoration(pos, end, { class: 'my-paragraph' })] + }, + shouldUpdate: ({ tr }) => tr.docChanged, + }), +}) +``` + +Widget example (using the helper): + +```js +import { Extension, createWidgetDecoration } from '@tiptap/core' + +export const StarAfter = Extension.create({ + name: 'starAfter', + + decorations: () => ({ + create() { + return [ + createWidgetDecoration(10, () => { + const el = document.createElement('span') + el.textContent = ' ⭐' + el.setAttribute('contenteditable', 'false') + return el + }), + ] + }, + shouldUpdate: ({ tr }) => tr.docChanged, + }), +}) +``` + +## Utility functions + +- `createInlineDecoration(from, to, attributes)` - returns a small object describing an inline decoration. +- `createNodeDecoration(from, to, attributes)` - returns a node decoration object. +- `createWidgetDecoration(at, widget)` - returns a widget item where `widget` is a function that creates a DOM node. + +Use these helpers for concise examples; under the hood Tiptap maps these items to ProseMirror decorations. + +## Create a decoration without helpers + +You can return the plain decoration items directly if you prefer to avoid helpers. The shape is intentionally small and simple: + +```js +// manual inline decoration +return [{ type: 'inline', from: 5, to: 11, attributes: { style: 'background: yellow' } }] + +// manual widget +return [ + { + type: 'widget', + from: 20, + to: 20, + widget: () => { + const el = document.createElement('button') + el.textContent = 'Click' + el.setAttribute('contenteditable', 'false') + return el + }, + }, +] +``` + +Notes: + +- `from`/`to` are document positions. +- For widgets, return a DOM node from the `widget` function. If you mount React components, return a container element and use the React widget helper from the React package. + +## Best practices & performance + +- Use `shouldUpdate` to limit recalculation. A common simple implementation is `({ tr }) => tr.docChanged`. +- Keep `spec` data small (strings/numbers) if you add it to items - it's used to decide whether a decoration meaning changed. +- Avoid scanning the whole document every transaction. Narrow traversal to nodes of interest or cache results per node when possible. +- For widgets, provide a stable `spec.key` (or ensure your widget markup is stable) to avoid unnecessary remounts. + +More about `spec` and stability + +- The `spec` object on a decoration is used to decide whether a decoration's meaning changed between renders. Keep it tiny and primitive (strings, numbers, booleans). Avoid functions, DOM nodes, or large objects inside `spec`. +- For widgets, include a stable identifier in `spec` (for example `spec.key: 'comment-123'`) so the renderer can reuse the same DOM node across updates and avoid remounting React components. +- Do NOT rely only on document positions for stability. Positions move when the doc changes. Prefer a stable id from the node (for example an `id` in `node.attrs`) or a key you manage on the extension side. + +Short widget example with a stable key: + +```js +return [ + { + type: 'widget', + from: pos, + to: pos, + widget: () => { + const el = document.createElement('span') + el.textContent = '⭐' + el.setAttribute('contenteditable', 'false') + return el + }, + spec: { key: 'my-widget-42' }, + }, +] +``` + +- If you need to compute keys from node content, compute a stable id once and store it on the node (attrs/marks) or in a side map. Recomputing ephemeral keys on every `create` will cause remounts. +- Combine sensible `shouldUpdate` logic with stable `spec` values: `shouldUpdate` decides when to recreate the list, `spec` decides whether individual decorations should be updated/reused. + +## Troubleshooting + +- Widgets flicker or lose internal state: give widgets stable keys (via `spec.key`) or avoid remounting DOM nodes. +- Widget callbacks see wrong positions after edits: call the provided `getPos()` in widget callbacks (or use the editor APIs) rather than relying on an earlier captured `pos`. +- Decorations don't update: check `shouldUpdate` and ensure it returns `true` for transactions that should trigger an update. + diff --git a/src/content/editor/getting-started/install/cdn.mdx b/src/content/editor/getting-started/install/cdn.mdx deleted file mode 100644 index 7e1fb4508..000000000 --- a/src/content/editor/getting-started/install/cdn.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: CDN -meta: - title: CDN | Tiptap Editor Docs - description: Learn how to use Tiptap via CDN for quick and easy setup in demos or tests. Explore our quick start guide in the docs! - category: Editor ---- - -For testing purposes or demos, use our esm.sh CDN builds. Here are a few lines of code you need to get started. - -```html - - - - - - -
- - - -``` - -Tiptap should now be visible in your browser. Time to give yourself a pat on the back! :) - -## Next steps - -- [Configure your editor](/editor/getting-started/configure) -- [Add styles to your editor](/editor/getting-started/style-editor) -- [Learn more about Tiptaps concepts](/editor/core-concepts/introduction) -- [Learn how to persist the editor state](/editor/core-concepts/persistence) -- [Start building your own extensions](/editor/extensions/custom-extensions) diff --git a/src/content/editor/getting-started/install/vanilla-javascript.mdx b/src/content/editor/getting-started/install/vanilla-javascript.mdx index 2c06f1794..51e503206 100644 --- a/src/content/editor/getting-started/install/vanilla-javascript.mdx +++ b/src/content/editor/getting-started/install/vanilla-javascript.mdx @@ -8,15 +8,23 @@ category: Editor import { Callout } from '@/components/ui/Callout' -Are you using plain JavaScript or a framework that isn't listed? No worries, we provide everything you need. +Are you building without a frontend framework like React or Vue? No problem, you can use Tiptap directly in plain JavaScript. - If you don't use a bundler like Webpack or Rollup, please follow the [CDN](/editor/getting-started/install/cdn) guide instead. Since Tiptap is built in a modular way, you will need to use ` + +
+``` ## Next steps diff --git a/src/content/editor/sidebar.ts b/src/content/editor/sidebar.ts index 8a8f778ea..b89b7eadc 100644 --- a/src/content/editor/sidebar.ts +++ b/src/content/editor/sidebar.ts @@ -513,6 +513,10 @@ export const sidebarConfig: SidebarConfig = { href: '/editor/core-concepts/schema', title: 'Schema', }, + { + href: '/editor/core-concepts/decorations', + title: 'Decorations', + }, { href: '/editor/core-concepts/keyboard-shortcuts', title: 'Keyboard shortcuts',