Skip to content

Tiptap RTE: Adds support for Reusable Content of Blocks - #23195

Open
leekelleher wants to merge 13 commits into
v19/devfrom
v19/feature/rcob-rte-blocks
Open

Tiptap RTE: Adds support for Reusable Content of Blocks#23195
leekelleher wants to merge 13 commits into
v19/devfrom
v19/feature/rcob-rte-blocks

Conversation

@leekelleher

@leekelleher leekelleher commented Jun 24, 2026

Copy link
Copy Markdown
Member

Description

Wires up the Tiptap RTE block extension to work with Element Library (reusable content) references, following the general Reusable Content of Blocks feature that landed in #22448.

Previously, inserting a Library Element into an RTE would silently do nothing — Library Elements only have a layout entry (no local contentData), so the block extension never detected them. This PR makes it all work.

Technical summary (AI/Claude generated) 🤖

  • RTE block extension now observes both local and external content, so library elements appear in the editor once their content resolves
  • Added a data-key attribute (the layout's unique key) to RTE block nodes so the same library element can be used multiple times independently — each instance is tracked and can be deleted/undone without affecting the others
  • Old markup without data-key continues to work (legacy-safe)
  • Library element blocks get the purple reference frame styling (is-reference)
  • Library element blocks are no longer incorrectly shown as "unpublished/greyed out"

Breaking changes

  • The editor.commands.setBlock() and editor.commands.setBlockInline() Tiptap commands now require an additional layoutKey parameter.

How to test

  1. Create a Document Type with a Rich Text property, and configure it to allow at least one Block Type
  2. Make sure you also have an Element Library set up with at least one published element
  3. Open a content node, go into the RTE, and insert a block from the Element Library tab — it should appear in the editor with a purple frame
  4. Insert the same library element twice — both instances should appear independently
  5. Delete one instance — only that one should disappear; the other stays
  6. Undo — the deleted instance comes back
  7. Load an existing RTE document that already has local blocks — they should still render correctly (no regression)

// CC'ing @kjac @lauraneto, as we may need to update the C# RegEx to support the new data-key attribute, so that parses RTE markup to get the block references.

- Add `allContents` observable to `UmbBlockManagerContext` merging local + external content
- Switch block observation from `context.contents` to `combineLatest([layouts, allContents])`; skips blocks whose content type hasn't resolved yet (external content arrives async)
- Add `data-key` attribute (layout key) to RTE block ProseMirror nodes so duplicate library element instances are tracked independently; `data-content-key` is retained for backwards-compat
- Legacy markup (no `data-key`) defaults `data-key = data-content-key` on parse, which is safe because pre-v19 data always has `layout.key === layout.contentKey`
- Switch `_filterUnusedBlocks` and undo stash from content-key to layout-key tracking; external blocks skip local content stash (their content lives in `#externalContentValues`)
- Rename `contentKey` params to `layoutKey` in `requestPendingDeletion`, `clearPendingDeletion`, `removeOneLayout`, `removeManyLayouts`, and `delete` — semantically safe for legacy data
- `umb-rte-block` exposes `data-key` as the primary property and deprecates `data-content-key` / `contentKey` (removal v21)
Copilot AI review requested due to automatic review settings June 24, 2026 14:15
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @leekelleher's task in 7m 53s —— View job


PR Review

Target: origin/v19/dev · Based on commit: ab054cb2 · Skipped: 0 noise files out of 9 total

Wires RTE blocks to the Element Library (reusable content) feature by switching the block-tracking key from content key to layout key throughout the frontend stack, and adds observers for external content state so library element blocks render with the correct is-reference styling and published/unpublished state.

  • Modified public API: UmbPropertyEditorUiRteElementBase._filterUnusedBlocks (protected, semantic change), UmbBlockRteManagerContext.requestPendingDeletion/clearPendingDeletion/removeOneLayout/removeManyLayouts (param rename), UmbBlockManagerContext.allContents (new observable), Tiptap setBlock/setBlockInline command options (added required layoutKey)
  • Other changes: HTML markup now includes data-key attribute alongside data-content-key; legacy markup (no data-key) continues to work via fallback in parseHTML and setLayouts coercion; :host CSS gains margin-top: 10px globally

Important

  • block-rte-entry.element.ts:391margin-top: 10px is added to :host, applying to all RTE block entries, not just library element references. This silently changes vertical spacing for every existing block in every RTE. Should be scoped to :host([is-reference]) if it's only needed for the reference frame. Fix this →

  • block.tiptap-extension.ts:7setBlock/setBlockInline command options now require layoutKey as a new required field. Any plugin-authored Tiptap extension calling editor.commands.setBlock({ contentKey }) will get a TypeScript compile error. This is a v19 major-version break, which is acceptable — worth noting in release/migration notes if these commands were previously documented or referenced publicly.

  • rte-base.element.ts:379_filterUnusedBlocks is a protected method on the exported abstract class UmbPropertyEditorUiRteElementBase. Its parameter semantics silently changed from content keys to layout keys — TypeScript cannot catch this because the type signature ((string | null)[]) is identical. Consider adding a JSDoc comment clarifying the expected key type to help plugin authors.

Suggestions

  • block.tiptap-api.ts:22combineLatest emits on every change to either observable; #updateBlocks can fire several times in rapid succession during initial load. The previous debounceTime(20) coalesced bursts. The existing deduplication prevents duplicate inserts, but a debounce would avoid the repeated DOM querySelectorAll calls.

  • block-manager.context.ts:95 — JSDoc says "Use this instead of contents…" which implies it should always be preferred. "Use this alongside contents when you also need to react to library elements becoming available" is more accurate.


Known follow-up flagged by author: The C# backend regex currently only extracts data-content-key from saved RTE markup. For local blocks this is fine (each has a unique content key). For library element blocks, multiple instances share the same data-content-key but have distinct data-key values — if the backend deduplicates by content key when reconstructing layout references, inserting the same library element twice could result in one instance being lost on save. This needs the C# side update (flagged for @kjac) before the full feature is safe to ship.


Request Changes

The margin-top CSS scope and the C# backend gap should be addressed. The command API and protected method changes are acceptable for v19 but deserve migration notes.

@leekelleher leekelleher changed the title Tiptap RTE Blocks: support Element Library (reusable content) references Tiptap RTE: Adds support for Reusable Content of Blocks Jun 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Tiptap RTE block integration to support Element Library (reusable/external) block references by keying RTE block instances on the layout key (data-key) rather than only the content key, and by observing external content resolution so referenced blocks appear once fetched.

Changes:

  • Add data-key (UMB_BLOCK_RTE_DATA_LAYOUT_KEY) to RTE block nodes (with legacy fallback) and switch block syncing/deletion to operate on layout keys.
  • Extend block manager state with allContents (local + external) and update the Tiptap block API to re-sync when external content resolves.
  • Update RTE block entry UI to style referenced blocks and compute published/unpublished state correctly for external content.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap-rte/property-editor-ui-tiptap.element.ts Extracts layout keys from markup to filter unused layouts correctly.
src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/block/block.tiptap-extension.ts Adds data-key attribute to block nodes and migrates legacy markup on parse.
src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/block/block.tiptap-api.ts Syncs editor DOM blocks from manager layouts and handles deletions by layout key; re-syncs when external content resolves.
src/Umbraco.Web.UI.Client/src/packages/rte/constants.ts Introduces UMB_BLOCK_RTE_DATA_LAYOUT_KEY = 'data-key'.
src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts Switches unused/restore tracking from content keys to layout keys and adjusts external-content handling.
src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts Adds allContents observable combining local and resolved external block content.
src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts Changes pending deletion tracking from content keys to layout keys.
src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts Updates delete API to request pending deletion by layout key.
src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts Adds data-key support, reference styling, and correct published state logic for external content.

Comment thread src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts Outdated
Comment thread src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts Outdated
@claude claude Bot added category/ux User experience category/ui User interface labels Jun 24, 2026
- Remove accidental margin-top regression on all RTE block entries
- Fix settings not stashed/restored for external blocks on delete+undo
- Reduce deep nesting in #restoreUnusedBlocks / #setUnusedBlockLookups
- De-duplicate getAttrs and insert-attrs logic in block.tiptap-extension.ts
- Clarify JSDoc on allContents and _filterUnusedBlocks parameter semantics
The umb-entity-frame label tab clips when a block is at the top of the
RTE without this spacing.
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts
Reduce cognitive complexity of #restoreUnusedBlocks by extracting
content/settings restoration into helper methods, use .includes()/Set.has()
over indexOf()/Array.includes(), and use RegExp.exec() over String.match().
@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

Adds a sibling test to the incoming v19/dev doc-walk test, asserting
the walk also finds layout keys (including the legacy data-key-missing
fallback), matching how #updateBlocks now discovers blocks post-merge.
Block RTE, Grid, List and Single entry elements each duplicated an
identical #updateExposedState() method and its three backing
observers. Move the derivation onto UmbBlockEntryContext as a single
mergeObservables-based isExposed observable, consumed the same way by
all four elements. Fixes a SonarQube new-code duplication gate failure
on block-rte-entry.element.ts.
These two cases were merged up from v17/dev before RTE blocks gained a
data-key layout attribute alongside data-content-key, so their
expected markup no longer matches the block node's actual (correct)
output. Use distinct data-key/data-content-key values to assert the
div-nesting behaviour without relying on the legacy same-value
back-fill, which is already covered by block.tiptap-extension.test.ts.
BlockRegex required data-content-key to appear immediately after the
tag name (or an optional class attribute), so it never matched once
data-key started being emitted before data-content-key. That left
RTE blocks rendering as unreplaced placeholders on the front end
after being re-saved through the editor. Widen the pattern to accept
attributes on either side of data-content-key.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
8.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

this.#blockTypes = new Map(
blockTypes?.map((x) => [x.contentElementTypeKey, x] as [string, UmbBlockRteTypeModel]),
);
combineLatest([context.layouts, context.allContents]),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this, we have a method called observeMultiple. It is the same, just a different export.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inserting a Library element into a Rich Text Editor block does not add the block to the editor

3 participants