@@ -64,7 +66,7 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement<
`;
}
- static override styles = [
+ static override readonly styles = [
css`
uui-input {
margin-bottom: var(--uui-size-layout-1);
@@ -74,6 +76,17 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement<
display: flex;
flex-direction: column;
}
+
+ #mainobject {
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+ }
`,
];
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts
rename to src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/Umbraco.RichText.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/Umbraco.RichText.ts
similarity index 93%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/Umbraco.RichText.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/Umbraco.RichText.ts
index 3dbe86e0a0..36fb6b3c94 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/Umbraco.RichText.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/Umbraco.RichText.ts
@@ -5,7 +5,7 @@ export const manifest: ManifestPropertyEditorSchema = {
name: 'Rich Text',
alias: 'Umbraco.RichText',
meta: {
- defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE',
+ defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap',
settings: {
properties: [
{
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts
similarity index 53%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts
index 55361eb8cd..c7e01a7dd5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts
@@ -1,4 +1,5 @@
-import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
+import { type UmbPropertyEditorUiValueType, UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../types.js';
+import { property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type {
@@ -7,31 +8,12 @@ import type {
} from '@umbraco-cms/backoffice/property-editor';
import {
UmbBlockRteEntriesContext,
- type UmbBlockRteLayoutModel,
UmbBlockRteManagerContext,
type UmbBlockRteTypeModel,
} from '@umbraco-cms/backoffice/block-rte';
-import type { UmbBlockValueType } from '@umbraco-cms/backoffice/block';
import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
-import '../../components/input-tiny-mce/input-tiny-mce.element.js';
-import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
-import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs';
-
-export interface UmbRichTextEditorValueType {
- markup: string;
- blocks: UmbBlockValueType
;
-}
-
-const UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS = 'Umbraco.RichText'; // Not rich text, cause this has not been migrated [NL]
-
-/**
- * @element umb-property-editor-ui-tiny-mce
- */
-@customElement('umb-property-editor-ui-tiny-mce')
-export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements UmbPropertyEditorUiElement {
- //
- // No need to registerer as a LIT-property, as we are calling it directly and no need for it to be reactive [NL]
+export abstract class UmbRteBaseElement extends UmbLitElement implements UmbPropertyEditorUiElement {
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
@@ -43,55 +25,66 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
this.#managerContext.setEditorConfiguration(config);
}
- @property({ attribute: false })
- public set value(value: UmbRichTextEditorValueType | undefined) {
- const buildUpValue: Partial = value ? { ...value } : {};
+ @property({
+ attribute: false,
+ type: Object,
+ hasChanged(value?: UmbPropertyEditorUiValueType, oldValue?: UmbPropertyEditorUiValueType) {
+ return value?.markup !== oldValue?.markup;
+ },
+ })
+ public set value(value: UmbPropertyEditorUiValueType | undefined) {
+ const buildUpValue: Partial = value ? { ...value } : {};
buildUpValue.markup ??= '';
buildUpValue.blocks ??= { layout: {}, contentData: [], settingsData: [], expose: [] };
buildUpValue.blocks.layout ??= {};
buildUpValue.blocks.contentData ??= [];
buildUpValue.blocks.settingsData ??= [];
buildUpValue.blocks.expose ??= [];
- this._value = buildUpValue as UmbRichTextEditorValueType;
+ this._value = buildUpValue as UmbPropertyEditorUiValueType;
+ // Only update the actual editor markup if it is not the same as the value.
if (this._latestMarkup !== this._value.markup) {
this._markup = this._value.markup;
}
- this.#managerContext.setLayouts(buildUpValue.blocks.layout[UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS] ?? []);
+ this.#managerContext.setLayouts(buildUpValue.blocks.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS] ?? []);
this.#managerContext.setContents(buildUpValue.blocks.contentData);
this.#managerContext.setSettings(buildUpValue.blocks.settingsData);
this.#managerContext.setExposes(buildUpValue.blocks.expose);
}
- public get value(): UmbRichTextEditorValueType {
+ public get value() {
return this._value;
}
/**
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
- * @type {boolean}
- * @attr
* @default false
*/
@property({ type: Boolean, reflect: true })
readonly = false;
@state()
- _config?: UmbPropertyEditorConfigCollection;
+ protected _config?: UmbPropertyEditorConfigCollection;
@state()
- private _value: UmbRichTextEditorValueType = {
+ protected _value: UmbPropertyEditorUiValueType = {
markup: '',
blocks: { layout: {}, contentData: [], settingsData: [], expose: [] },
};
- // Separate state for markup, to avoid re-rendering/re-setting the value of the TinyMCE editor when the value does not really change.
+ /**
+ * Separate state for markup, to avoid re-rendering/re-setting the value of the Tiptap editor when the value does not really change.
+ */
@state()
- private _markup = '';
- private _latestMarkup = ''; // The latest value gotten from the TinyMCE editor.
+ protected _markup = '';
- #managerContext = new UmbBlockRteManagerContext(this);
- #entriesContext = new UmbBlockRteEntriesContext(this);
+ /**
+ * The latest value gotten from the RTE editor.
+ */
+ protected _latestMarkup = '';
+
+ readonly #managerContext = new UmbBlockRteManagerContext(this);
+ readonly #entriesContext = new UmbBlockRteEntriesContext(this);
constructor() {
super();
@@ -125,7 +118,34 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
'observePropertyAlias',
);
- this.observe(
+ this.observe(this.#entriesContext.layoutEntries, (layouts) => {
+ // Update manager:
+ this.#managerContext.setLayouts(layouts);
+ });
+
+ // Observe the value of the property and update the editor value.
+ this.observe(this.#managerContext.layouts, (layouts) => {
+ this._value = {
+ ...this._value,
+ blocks: { ...this._value.blocks, layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts } },
+ };
+ this._fireChangeEvent();
+ });
+ this.observe(this.#managerContext.contents, (contents) => {
+ this._value = { ...this._value, blocks: { ...this._value.blocks, contentData: contents } };
+ this._fireChangeEvent();
+ });
+ this.observe(this.#managerContext.settings, (settings) => {
+ this._value = { ...this._value, blocks: { ...this._value.blocks, settingsData: settings } };
+ this._fireChangeEvent();
+ });
+ this.observe(this.#managerContext.exposes, (exposes) => {
+ this._value = { ...this._value, blocks: { ...this._value.blocks, expose: exposes } };
+ this._fireChangeEvent();
+ });
+
+ // The above could potentially be replaced with a single observeMultiple call, but it is not done for now to avoid potential issues with the order of the updates.
+ /*this.observe(
observeMultiple([
this.#managerContext.layouts,
this.#managerContext.contents,
@@ -136,16 +156,17 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
this._value = {
...this._value,
blocks: {
- layout: { [UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS]: layouts },
+ layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts },
contentData: contents,
settingsData: settings,
expose: exposes,
},
};
- context.setValue(this._value);
+
+ this._fireChangeEvent();
},
'motherObserver',
- );
+ );*/
});
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
this.#managerContext.setVariantId(context.getVariantId());
@@ -157,59 +178,18 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
});
}
- #onChange() {
- const editor = this.#managerContext.getTinyMceEditor();
- if (!editor) return;
-
- // Clone the DOM, to remove the classes and attributes on the original:
- const div = document.createElement('div');
- div.innerHTML = editor.getContent();
-
- // Loop through used, to remove the classes on these.
- const blockEls = div.querySelectorAll(`umb-rte-block, umb-rte-block-inline`);
- blockEls.forEach((blockEl) => {
- blockEl.removeAttribute('contenteditable');
- blockEl.removeAttribute('class');
+ protected _filterUnusedBlocks(usedContentKeys: (string | null)[]) {
+ const unusedBlockContents = this.#managerContext.getContents().filter((x) => usedContentKeys.indexOf(x.key) === -1);
+ unusedBlockContents.forEach((blockContent) => {
+ this.#managerContext.removeOneContent(blockContent.key);
});
-
- const markup = div.innerHTML;
-
- // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup.
- //const blockElements = editor.dom.select(`umb-rte-block, umb-rte-block-inline`);
- const usedContentKeys = Array.from(blockEls).map((blockElement) => blockElement.getAttribute('data-content-key'));
const unusedBlocks = this.#managerContext.getLayouts().filter((x) => usedContentKeys.indexOf(x.contentKey) === -1);
unusedBlocks.forEach((blockLayout) => {
this.#managerContext.removeOneLayout(blockLayout.contentKey);
});
+ }
- // Then get the content of the editor and update the value.
- // maybe in this way doc.body.innerHTML;
-
- this._latestMarkup = markup;
-
- this._value = {
- ...this._value,
- markup: markup,
- };
+ protected _fireChangeEvent() {
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
-
- override render() {
- return html`
-
-
- `;
- }
-}
-
-export default UmbPropertyEditorUITinyMceElement;
-
-declare global {
- interface HTMLElementTagNameMap {
- 'umb-property-editor-ui-tiny-mce': UmbPropertyEditorUITinyMceElement;
- }
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/manifests.ts
new file mode 100644
index 0000000000..bfeeb1e04b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/manifests.ts
@@ -0,0 +1,13 @@
+// eslint-disable-next-line local-rules/no-relative-import-to-import-map-module
+import { manifests as tiptapManifests } from './tiptap/manifests.js';
+import { manifests as tinyMceManifests } from './tiny-mce/manifests.js';
+import { manifest as schemaManifest } from './Umbraco.RichText.js';
+import { manifest as blockRtePropertyValueResolver } from './property-value-resolver/manifest.js';
+import type { ManifestTypes, UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
+
+export const manifests: Array = [
+ ...tinyMceManifests,
+ ...tiptapManifests,
+ schemaManifest,
+ blockRtePropertyValueResolver,
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json b/src/Umbraco.Web.UI.Client/src/packages/rte/package.json
similarity index 66%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json
rename to src/Umbraco.Web.UI.Client/src/packages/rte/package.json
index 6362dc5347..20394e7e36 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/package.json
@@ -1,5 +1,5 @@
{
- "name": "@umbraco-backoffice/tiny-mce",
+ "name": "@umbraco-backoffice/rte",
"private": true,
"type": "module",
"scripts": {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/manifest.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/manifest.ts
new file mode 100644
index 0000000000..d9c9b061f0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/manifest.ts
@@ -0,0 +1,13 @@
+import { UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../types.js';
+import { UmbRteBlockValueResolver } from './rte-block-value-resolver.api.js';
+import type { ManifestPropertyValueResolver } from '@umbraco-cms/backoffice/property';
+
+export const manifest: ManifestPropertyValueResolver = {
+ type: 'propertyValueResolver',
+ alias: 'Umb.PropertyValueResolver.RichTextBlocks',
+ name: 'Block Value Resolver',
+ api: UmbRteBlockValueResolver,
+ meta: {
+ editorAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS,
+ },
+};
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-value-resolver/rte-block-value-resolver.api.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/rte-block-value-resolver.api.ts
similarity index 94%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-value-resolver/rte-block-value-resolver.api.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/rte-block-value-resolver.api.ts
index edf2726889..fbf8f45d17 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-value-resolver/rte-block-value-resolver.api.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/rte-block-value-resolver.api.ts
@@ -1,4 +1,4 @@
-import type { UmbPropertyEditorUiValueType } from '../../../types.js';
+import type { UmbPropertyEditorUiValueType } from '../types.js';
import {
UmbBlockValueResolver,
type UmbBlockDataValueModel,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/index.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/index.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/index.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/index.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/index.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/index.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.defaults.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.defaults.ts
new file mode 100644
index 0000000000..45705a7ec5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.defaults.ts
@@ -0,0 +1,153 @@
+import { UMB_CONTENT_REQUEST_EVENT_TYPE, type UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api';
+import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce';
+import { UUIIconRequestEvent } from '@umbraco-cms/backoffice/external/uui';
+
+//export const UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH = '/umbraco/backoffice/packages/block/block-rte/index.js';
+export const UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH = '@umbraco-cms/backoffice/block-rte';
+
+//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
+//so we don't have to specify all the normal elements again
+export const defaultFallbackConfig: RawEditorOptions = {
+ plugins: ['anchor', 'charmap', 'table', 'lists', 'advlist', 'autolink', 'directionality', 'searchreplace'],
+ valid_elements:
+ '+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*]',
+ invalid_elements: 'font',
+ extended_valid_elements:
+ '@[id|class|style],+umb-rte-block[!data-content-key],+umb-rte-block-inline[!data-content-key],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption',
+ custom_elements: 'umb-rte-block,~umb-rte-block-inline',
+ toolbar: [
+ 'styles',
+ 'bold',
+ 'italic',
+ 'alignleft',
+ 'aligncenter',
+ 'alignright',
+ 'bullist',
+ 'numlist',
+ 'outdent',
+ 'indent',
+ 'link',
+ 'umbmediapicker',
+ 'umbembeddialog',
+ ],
+
+ init_instance_callback: function (editor) {
+ // The following code is the context api proxy. [NL]
+ // It re-dispatches the context api request event to the origin target of this modal, in other words the element that initiated the modal. [NL]
+ editor.dom.doc.addEventListener(UMB_CONTENT_REQUEST_EVENT_TYPE, ((event: UmbContextRequestEvent) => {
+ if (!editor.iframeElement) return;
+
+ event.stopImmediatePropagation();
+ editor.iframeElement.dispatchEvent(event.clone());
+ }) as EventListener);
+
+ // Proxy for retrieving icons from outside the iframe [NL]
+ editor.dom.doc.addEventListener(UUIIconRequestEvent.ICON_REQUEST, ((event: UUIIconRequestEvent) => {
+ if (!editor.iframeElement) return;
+
+ const newEvent = new UUIIconRequestEvent(UUIIconRequestEvent.ICON_REQUEST, {
+ detail: event.detail,
+ });
+ editor.iframeElement.dispatchEvent(newEvent);
+ if (newEvent.icon !== null) {
+ event.acceptRequest(newEvent.icon);
+ }
+ }) as EventListener);
+
+ // Transfer our import-map to the iframe: [NL]
+ const importMapTag = document.head.querySelector('script[type="importmap"]');
+ if (importMapTag) {
+ const importMap = document.createElement('script');
+ importMap.type = 'importmap';
+ importMap.text = importMapTag.innerHTML;
+ editor.dom.doc.head.appendChild(importMap);
+ }
+
+ // Transfer our stylesheets to the iframe: [NL]
+ const stylesheetTags = document.head.querySelectorAll('link[rel="stylesheet"]');
+ stylesheetTags.forEach((stylesheetTag) => {
+ const stylesheet = document.createElement('link');
+ stylesheet.rel = 'stylesheet';
+ stylesheet.href = stylesheetTag.href;
+ editor.dom.doc.head.appendChild(stylesheet);
+ });
+
+ editor.dom.doc.addEventListener('click', (e: MouseEvent) => {
+ // If we try to open link in a new tab, then we want to skip skip:
+ //if ((isWindows && e.ctrlKey) || (!isWindows && e.metaKey)) return;
+
+ // Find the target by using the composed path to get the element through the shadow boundaries.
+ // Notice the difference here compared to RouterSlots implementation [NL]
+ const $anchor: HTMLAnchorElement = (('composedPath' in e) as any)
+ ? (e
+ .composedPath()
+ .find(($elem) => $elem instanceof HTMLAnchorElement || ($elem as any).tagName === 'A') as HTMLAnchorElement)
+ : (e.target as HTMLAnchorElement);
+
+ // Abort if the event is not about the anchor tag
+ if ($anchor == null || !($anchor instanceof HTMLAnchorElement || ($anchor as any).tagName === 'A')) {
+ return;
+ }
+
+ // Get the HREF value from the anchor tag
+ const href = $anchor.href;
+
+ // Only handle the anchor tag if the follow holds true:
+ // - The HREF is relative to the origin of the current location.
+ // - The target is targeting the current frame.
+ // - The anchor doesn't have the attribute [data-router-slot]="disabled"
+ if (
+ !href.startsWith(location.origin) ||
+ ($anchor.target !== '' && $anchor.target !== '_self') ||
+ $anchor.dataset['routerSlot'] === 'disabled'
+ ) {
+ return;
+ }
+
+ // Remove the origin from the start of the HREF to get the path
+ const path = $anchor.pathname + $anchor.search + $anchor.hash;
+
+ // Prevent the default behavior
+ e.preventDefault();
+
+ // Change the history!
+ window.history.pushState(null, '', path);
+ });
+
+ // Load backoffice JS so we can get the umb-rte-block component registered inside the iframe [NL]
+ const script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.setAttribute('type', 'module');
+
+ script.text = `import "@umbraco-cms/backoffice/extension-registry";`;
+ script.text = `import "${UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH}";`;
+ editor.dom.doc.head.appendChild(script);
+ },
+
+ style_formats: [
+ {
+ title: 'Headers',
+ items: [
+ { title: 'Page header', block: 'h2' },
+ { title: 'Section header', block: 'h3' },
+ { title: 'Paragraph header', block: 'h4' },
+ ],
+ },
+ {
+ title: 'Blocks',
+ items: [{ title: 'Paragraph', block: 'p' }],
+ },
+ {
+ title: 'Containers',
+ items: [
+ { title: 'Quote', block: 'blockquote' },
+ { title: 'Code', block: 'code' },
+ ],
+ },
+ ],
+ /**
+ * @description The maximum image size in pixels that can be inserted into the editor.
+ * @remarks This is registered and used by the UmbMediaPicker plugin
+ */
+ maxImageSize: 500,
+};
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
similarity index 93%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
index 87601bc0a2..0a081ea5a8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
@@ -18,13 +18,13 @@ import {
renderEditor,
} from '@umbraco-cms/backoffice/external/tinymce';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
+import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { ManifestTinyMcePlugin } from '@umbraco-cms/backoffice/tiny-mce';
/**
* Handles the resize event
* @param e
*/
-// TODO: This does somehow not belong as a utility method as it is very specific to this implementation. [NL]
async function onResize(
e: EditorEvent<{
target: HTMLElement;
@@ -43,7 +43,7 @@ async function onResize(
const resizedPath = await getProcessedImageUrl(path, {
width: e.width,
height: e.height,
- mode: 'max',
+ mode: ImageCropModeModel.MAX,
});
e.target.setAttribute('data-mce-src', resizedPath);
@@ -56,8 +56,8 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
#plugins: Array | undefined> = [];
#editorRef?: Editor | null = null;
- #stylesheetRepository = new UmbStylesheetDetailRepository(this);
- #umbStylesheetRuleManager = new UmbStylesheetRuleManager();
+ readonly #stylesheetRepository = new UmbStylesheetDetailRepository(this);
+ readonly #umbStylesheetRuleManager = new UmbStylesheetRuleManager();
protected override getFormElement() {
return this._editorElement?.querySelector('iframe') ?? undefined;
@@ -65,7 +65,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
override set value(newValue: FormDataEntryValue | FormData) {
super.value = newValue;
- const newContent = newValue?.toString() ?? '';
+ const newContent = typeof newValue === 'string' ? newValue : '';
if (this.#editorRef && this.#editorRef.getContent() != newContent) {
this.#editorRef.setContent(newContent);
@@ -95,14 +95,13 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
#readonly = false;
@query('.editor', true)
- private _editorElement?: HTMLElement;
+ private readonly _editorElement?: HTMLElement;
getEditor() {
return this.#editorRef;
}
- constructor() {
- super();
+ override firstUpdated() {
this.#loadEditor();
}
@@ -228,8 +227,8 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
// set the configured toolbar if any, otherwise false
const toolbar = this.configuration?.getValueByAlias('toolbar');
- if (toolbar && toolbar.length) {
- configurationOptions.toolbar = toolbar?.join(' ');
+ if (toolbar?.length) {
+ configurationOptions.toolbar = toolbar.join(' ');
} else {
configurationOptions.toolbar = false;
}
@@ -337,13 +336,12 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
/**
* Prevent injecting arbitrary JavaScript execution in on-attributes.
*
- * TODO: This used to be toggleable through server variables with window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce
*/
const allNodes = Array.from(editor.dom.doc.getElementsByTagName('*'));
allNodes.forEach((node) => {
- for (let i = 0; i < node.attributes.length; i++) {
- if (node.attributes[i].name.startsWith('on')) {
- node.removeAttribute(node.attributes[i].name);
+ for (const attr of node.attributes) {
+ if (attr.name.startsWith('on')) {
+ node.removeAttribute(attr.name);
}
}
});
@@ -365,7 +363,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
//enable browser based spell checking
editor.getBody().setAttribute('spellcheck', 'true');
uriAttributeSanitizer(editor);
- editor.setContent(this.value?.toString() ?? '');
+ editor.setContent(typeof this.value === 'string' ? this.value : '');
}
#onChange(value: string) {
@@ -381,7 +379,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
return html``;
}
- static override styles = [
+ static override readonly styles = [
css`
.tox-tinymce {
position: relative;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts
similarity index 61%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts
index fbc5e0ebd9..d50a6ef6e3 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts
@@ -1,4 +1,3 @@
-// TODO: clean up this file
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { Editor } from '@umbraco-cms/backoffice/external/tinymce';
@@ -28,13 +27,7 @@ export const uriAttributeSanitizer = (editor: Editor) => {
return function parseUri(uri: string, tagName: string) {
uri = uri.replace(trimRegExp, '');
- try {
- // Might throw malformed URI sequence
- uri = decodeURIComponent(uri);
- } catch (ex) {
- // Fallback to non UTF-8 decoder
- uri = unescape(uri);
- }
+ uri = decodeURIComponent(uri);
if (scriptUriRegExp.test(uri)) {
return;
@@ -48,20 +41,15 @@ export const uriAttributeSanitizer = (editor: Editor) => {
};
})();
- // TODO: sanitizeTinyMce is not defined in the global scope, so this will not work. Instead we need to get this setting from somewhere else:
- /*
- if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) {
- uriAttributesToSanitize.forEach((attribute) => {
- editor.serializer.addAttributeFilter(attribute, (nodes: AstNode[]) => {
- nodes.forEach((node: AstNode) => {
- node.attributes?.forEach((attr) => {
- if (uriAttributesToSanitize.includes(attr.name.toLowerCase())) {
- attr.value = parseUri(attr.value, node.name) ?? '';
- }
- });
- });
- });
+ editor.serializer.addAttributeFilter('uriAttributesToSanitize', function (nodes) {
+ nodes.forEach(function (node) {
+ if (!node.attributes) return;
+ for (const attr of node.attributes) {
+ const attrName = attr.name.toLowerCase();
+ if (uriAttributesToSanitize.indexOf(attrName) !== -1) {
+ attr.value = parseUri(attr.value, node.name) ?? '';
+ }
+ }
});
- }
- */
+ });
};
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/index.ts
similarity index 68%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/index.ts
index d303eda538..fad1d3710a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/index.ts
@@ -1,3 +1,2 @@
export * from './components/index.js';
-export * from './modals/index.js';
export * from './plugins/index.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/manifests.ts
similarity index 65%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/manifests.ts
index 7c24fabe3e..0c978efd63 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/manifests.ts
@@ -1,5 +1,4 @@
import { manifests as propertyEditors } from './property-editors/manifests.js';
import { manifests as plugins } from './plugins/manifests.js';
-import { manifests as modalManifests } from './modals/manifests.js';
-export const manifests: Array = [...propertyEditors, ...plugins, ...modalManifests];
+export const manifests: Array = [...propertyEditors, ...plugins];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/index.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/index.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/index.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/manifests.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/manifests.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/manifests.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts
similarity index 56%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts
index 43265dca4d..ddfc1e5a01 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts
@@ -1,18 +1,26 @@
-import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from '../../block/block-rte/context/block-rte-manager.context-token.js';
-import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '../../block/block-rte/context/block-rte-entries.context-token.js';
+import { UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '../../types.js';
import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/tiny-mce';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
+import type { Editor } from '@umbraco-cms/backoffice/external/tinymce';
+import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block';
+import {
+ UMB_BLOCK_RTE_ENTRIES_CONTEXT,
+ UMB_BLOCK_RTE_MANAGER_CONTEXT,
+ type UmbBlockRteLayoutModel,
+} from '@umbraco-cms/backoffice/block-rte';
export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase {
- #localize = new UmbLocalizationController(this._host);
-
- private _blocks?: Array;
+ readonly #localize = new UmbLocalizationController(this._host);
+ readonly #editor: Editor;
+ #blocks?: Array;
#entriesContext?: typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE;
constructor(args: TinyMcePluginArguments) {
super(args);
+ this.#editor = args.editor;
+
args.editor.ui.registry.addToggleButton('umbblockpicker', {
icon: 'visualblocks',
tooltip: this.#localize.term('blockEditor_insertBlock'),
@@ -27,15 +35,21 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase
});
this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => {
- context.setTinyMceEditor(args.editor);
-
this.observe(
context.blockTypes,
(blockTypes) => {
- this._blocks = blockTypes;
+ this.#blocks = blockTypes;
},
'blockType',
);
+
+ this.observe(
+ context.contents,
+ (contents) => {
+ this.#updateBlocks(contents, context.getLayouts());
+ },
+ 'contents',
+ );
});
this.consumeContext(UMB_BLOCK_RTE_ENTRIES_CONTEXT, (context) => {
this.#entriesContext = context;
@@ -64,11 +78,10 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase
return;
}
- // TODO: Missing solution to skip catalogue if only one type available. [NL]
let createPath: string | undefined = undefined;
- if (this._blocks?.length === 1) {
- const elementKey = this._blocks[0].contentElementTypeKey;
+ if (this.#blocks?.length === 1) {
+ const elementKey = this.#blocks[0].contentElementTypeKey;
createPath = this.#entriesContext.getPathForCreateBlock() + 'modal/umb-modal-workspace/create/' + elementKey;
} else {
createPath = this.#entriesContext.getPathForCreateBlock();
@@ -78,4 +91,32 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase
window.history.pushState({}, '', createPath);
}
}
+
+ #updateBlocks(blocks: UmbBlockDataModel[], layouts: Array) {
+ const editor = this.#editor;
+ if (!editor?.dom) return;
+
+ const existingBlocks = editor.dom
+ .select('umb-rte-block, umb-rte-block-inline')
+ .map((x) => x.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY));
+ const newBlocks = blocks.filter((x) => !existingBlocks.find((contentKey) => contentKey === x.key));
+
+ newBlocks.forEach((block) => {
+ // Find layout for block
+ const layout = layouts.find((x) => x.contentKey === block.key);
+ const inline = layout?.displayInline ?? false;
+
+ let blockTag = 'umb-rte-block';
+
+ if (inline) {
+ blockTag = 'umb-rte-block-inline';
+ }
+
+ const blockEl = `<${blockTag} ${UMB_BLOCK_RTE_DATA_CONTENT_KEY}="${block.key}">${blockTag}>`;
+
+ editor.selection.setContent(blockEl);
+ editor.setDirty(true);
+ editor.dispatch('Change');
+ });
+ }
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts
similarity index 84%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts
index 91b79639a7..e7e4ca1556 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts
@@ -1,11 +1,15 @@
-import { getGuid } from '../utils.js';
-import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL } from '../modals/media-caption-alt-text/media-caption-alt-text-modal.token.js';
import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '../components/input-tiny-mce/tiny-mce-plugin.js';
+import { getGuidFromUdi } from '@umbraco-cms/backoffice/utils';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce';
import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file';
import { UmbId } from '@umbraco-cms/backoffice/id';
-import { sizeImageInEditor, uploadBlobImages, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media';
+import {
+ sizeImageInEditor,
+ uploadBlobImages,
+ UMB_MEDIA_PICKER_MODAL,
+ UMB_MEDIA_CAPTION_ALT_TEXT_MODAL,
+} from '@umbraco-cms/backoffice/media';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
interface MediaPickerTargetData {
@@ -27,7 +31,7 @@ interface MediaPickerResultData {
export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
#modalManager?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
- #temporaryFileRepository;
+ readonly #temporaryFileRepository;
constructor(args: TinyMcePluginArguments) {
super(args);
@@ -39,13 +43,6 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
this.#modalManager = instance;
});
- // TODO => this breaks tests. disabling for now
- // will ignore user media start nodes
- // this.host.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => {
- // this.#currentUserContext = instance;
- // this.#observeCurrentUser();
- // });
-
this.editor.ui.registry.addToggleButton('umbmediapicker', {
icon: 'image',
tooltip: localize.term('general_mediaPicker'),
@@ -120,7 +117,6 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
async #showMediaPicker(currentTarget: MediaPickerTargetData) {
/*
- // TODO: I dont think we should parse this one... it should be up to the modal to get this information, and then we could parse some configs on to affect this.
let startNodeId;
let startNodeIsVirtual;
@@ -135,31 +131,36 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
}
*/
- // TODO => startNodeId and startNodeIsVirtual do not exist on ContentTreeItemResponseModel
-
const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, {
data: {
multiple: false,
//startNodeIsVirtual,
},
value: {
- selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [],
+ selection: currentTarget.udi ? [getGuidFromUdi(currentTarget.udi)] : [],
},
});
if (!modalHandler) return;
const { selection } = await modalHandler.onSubmit().catch(() => ({ selection: undefined }));
- if (!selection || !selection.length) return;
+ if (!selection?.length) return;
- this.#showMediaCaptionAltText(selection[0]);
+ this.#showMediaCaptionAltText(selection[0], currentTarget);
this.editor.dispatch('Change');
}
- async #showMediaCaptionAltText(mediaUnique: string | null) {
+ async #showMediaCaptionAltText(mediaUnique: string | null, currentTarget: MediaPickerTargetData) {
if (!mediaUnique) return;
- const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, { data: { mediaUnique } });
+ const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, {
+ data: { mediaUnique },
+ value: {
+ url: '',
+ altText: currentTarget.altText,
+ caption: currentTarget.caption,
+ },
+ });
const mediaData = await modalHandler?.onSubmit().catch(() => null);
if (!mediaData) return;
@@ -199,13 +200,11 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
} else {
parentElement.innerHTML = combined;
}
- } else {
+ } else if (parentElement?.nodeName === 'FIGURE' && parentElement.parentElement) {
//if caption is removed, remove the figure element
- if (parentElement?.nodeName === 'FIGURE' && parentElement.parentElement) {
- parentElement.parentElement.innerHTML = newImage;
- } else {
- this.editor.selection.setContent(newImage);
- }
+ parentElement.parentElement.innerHTML = newImage;
+ } else {
+ this.editor.selection.setContent(newImage);
}
// Using settimeout to wait for a DoM-render, so we can find the new element by ID.
@@ -230,7 +229,7 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
});
}
- #uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => {
+ readonly #uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => {
return new Promise((resolve, reject) => {
// Fetch does not support progress, so we need to fake it.
progress(0);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tinymce-plugin.extension.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tinymce-plugin.extension.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/manifests.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/manifests.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/manifests.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts
similarity index 83%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts
index 2c10cd74a5..a830297e36 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts
@@ -1,14 +1,15 @@
-import { type UmbBlockTypeBaseModel, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';
-import { UMB_BLOCK_RTE_TYPE } from '@umbraco-cms/backoffice/block-rte';
-import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
-import { html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
-import {
- UmbPropertyValueChangeEvent,
- type UmbPropertyEditorConfigCollection,
-} from '@umbraco-cms/backoffice/property-editor';
+import { customElement, html, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
+import { UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
+import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
+import { UMB_BLOCK_RTE_TYPE } from '@umbraco-cms/backoffice/block-rte';
+import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
+import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
+import type {
+ UmbPropertyEditorUiElement,
+ UmbPropertyEditorConfigCollection,
+} from '@umbraco-cms/backoffice/property-editor';
/**
* @element umb-property-editor-ui-block-rte-type-configuration
@@ -18,7 +19,7 @@ export class UmbPropertyEditorUIBlockRteBlockConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
- #blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController<
+ readonly #blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController<
typeof UMB_WORKSPACE_MODAL.DATA,
typeof UMB_WORKSPACE_MODAL.VALUE
>;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts
similarity index 97%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts
index b8b9b82d8b..e47c74dce5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts
@@ -38,7 +38,7 @@ export class UmbPropertyEditorUITinyMceDimensionsConfigurationElement extends Um
pixels`;
}
- static override styles = [UmbTextStyles];
+ static override readonly styles = [UmbTextStyles];
}
export default UmbPropertyEditorUITinyMceDimensionsConfigurationElement;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts
similarity index 91%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts
index 70b2d39ad3..51322338cd 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts
@@ -1,6 +1,6 @@
+import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js';
import type { Meta } from '@storybook/web-components';
import './property-editor-ui-tiny-mce-dimensions-configuration.element.js';
-import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/manifests.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/manifests.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/manifests.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts
similarity index 90%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts
index 714136611c..652aa6769e 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts
@@ -1,4 +1,4 @@
-import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js';
+import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js';
import type { Meta } from '@storybook/web-components';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-editor-ui-tiny-mce-maximagesize.element.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts
similarity index 87%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts
index 43ed4bceb4..a0efa34c22 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts
@@ -1,9 +1,11 @@
-import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system';
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
-import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
+import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system';
+import type {
+ UmbPropertyEditorConfigCollection,
+ UmbPropertyEditorUiElement,
+} from '@umbraco-cms/backoffice/property-editor';
import type { UmbStylesheetInputElement } from '@umbraco-cms/backoffice/stylesheet';
/**
@@ -14,7 +16,7 @@ export class UmbPropertyEditorUITinyMceStylesheetsConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
- #serverFilePathUniqueSerializer = new UmbServerFilePathUniqueSerializer();
+ readonly #serverFilePathUniqueSerializer = new UmbServerFilePathUniqueSerializer();
@property({ type: Array })
public set value(value: Array) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts
similarity index 91%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts
index 1341c768a7..0c6292c87b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts
@@ -1,4 +1,4 @@
-import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js';
+import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js';
import type { Meta } from '@storybook/web-components';
import { html } from '@umbraco-cms/backoffice/external/lit';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/manifests.ts
similarity index 90%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/manifests.ts
index 5577fe4db2..c65992bdca 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/manifests.ts
@@ -1,7 +1,5 @@
-import { manifest as schemaManifest } from './Umbraco.RichText.js';
-import { UmbRteBlockValueResolver } from './property-value-resolver/rte-block-value-resolver.api.js';
+import { UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../types.js';
-export const UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS = 'Umbraco.RichText';
export const UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS = 'Umb.PropertyEditorUi.TinyMCE';
export const manifests: Array = [
@@ -11,7 +9,7 @@ export const manifests: Array = [
name: 'Rich Text Editor Property Editor UI',
element: () => import('./property-editor-ui-tiny-mce.element.js'),
meta: {
- label: 'Rich Text Editor',
+ label: 'Rich Text Editor [TinyMCE]',
propertyEditorSchemaAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS,
icon: 'icon-browser-window',
group: 'richContent',
@@ -134,14 +132,4 @@ export const manifests: Array = [
},
},
},
- {
- type: 'propertyValueResolver',
- alias: 'Umb.PropertyValueResolver.TinyMce',
- name: 'Block Value Resolver',
- api: UmbRteBlockValueResolver,
- meta: {
- editorAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS,
- },
- },
- schemaManifest,
];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts
new file mode 100644
index 0000000000..2d1064c461
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts
@@ -0,0 +1,68 @@
+import { UmbRteBaseElement } from '../../../components/rte-base.element.js';
+import { UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '../../../types.js';
+import type { UmbInputTinyMceElement } from '../../components/input-tiny-mce/input-tiny-mce.element.js';
+import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
+
+import '../../components/input-tiny-mce/input-tiny-mce.element.js';
+
+/**
+ * @element umb-property-editor-ui-tiny-mce
+ */
+@customElement('umb-property-editor-ui-tiny-mce')
+export class UmbPropertyEditorUITinyMceElement extends UmbRteBaseElement {
+ #onChange(event: CustomEvent & { target: UmbInputTinyMceElement }) {
+ const value = typeof event.target.value === 'string' ? event.target.value : '';
+
+ // Clone the DOM, to remove the classes and attributes on the original:
+ const div = document.createElement('div');
+ div.innerHTML = value;
+
+ // Loop through used, to remove the classes on these.
+ const blockEls = div.querySelectorAll(`umb-rte-block, umb-rte-block-inline`);
+ blockEls.forEach((blockEl) => {
+ blockEl.removeAttribute('contenteditable');
+ blockEl.removeAttribute('class');
+ });
+
+ const markup = div.innerHTML;
+
+ // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup.
+ //const blockElements = editor.dom.select(`umb-rte-block, umb-rte-block-inline`);
+ const usedContentKeys = Array.from(blockEls).map((blockElement) =>
+ blockElement.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY),
+ );
+
+ this._filterUnusedBlocks(usedContentKeys);
+
+ // Then get the content of the editor and update the value.
+ // maybe in this way doc.body.innerHTML;
+
+ this._latestMarkup = markup;
+
+ this._value = {
+ ...this._value,
+ markup: markup,
+ };
+
+ this._fireChangeEvent();
+ }
+
+ override render() {
+ return html`
+
+
+ `;
+ }
+}
+
+export default UmbPropertyEditorUITinyMceElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-property-editor-ui-tiny-mce': UmbPropertyEditorUITinyMceElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts
similarity index 92%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts
index af902002c4..9559126d33 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts
@@ -1,15 +1,15 @@
+import { css, customElement, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
+import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
+import { tinymce } from '@umbraco-cms/backoffice/external/tinymce';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
-import { customElement, css, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
-import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
-import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
-import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
-import {
- UmbPropertyValueChangeEvent,
- type UmbPropertyEditorConfigCollection,
+import type {
+ UmbPropertyEditorUiElement,
+ UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
-import { tinymce } from '@umbraco-cms/backoffice/external/tinymce';
const tinyIconSet = tinymce.IconManager.get('default');
@@ -59,7 +59,7 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement
config?: UmbPropertyEditorConfigCollection;
@state()
- private _toolbarConfig: ToolbarConfig[] = [];
+ private readonly _toolbarConfig: ToolbarConfig[] = [];
#selectedValues: string[] = [];
@@ -128,7 +128,7 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement
`;
}
- static override styles = [
+ static override readonly styles = [
UmbTextStyles,
css`
ul {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts
similarity index 91%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts
index 29f8543717..aa598bf356 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts
@@ -1,4 +1,4 @@
-import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js';
+import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js';
import type { Meta } from '@storybook/web-components';
import { html } from '@umbraco-cms/backoffice/external/lit';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts
rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/index.ts
new file mode 100644
index 0000000000..f03903fb07
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/index.ts
@@ -0,0 +1,2 @@
+export * from './input-tiptap/index.js';
+export * from './toolbar/tiptap-toolbar-dropdown-base.element.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/index.ts
new file mode 100644
index 0000000000..f11b53c7d9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/index.ts
@@ -0,0 +1 @@
+export * from './input-tiptap.element.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/input-tiptap.element.ts
new file mode 100644
index 0000000000..dee7a0609d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/input-tiptap.element.ts
@@ -0,0 +1,329 @@
+import type { UmbTiptapExtensionApi, UmbTiptapToolbarValue } from '../../extensions/types.js';
+import { css, customElement, html, property, state, when } from '@umbraco-cms/backoffice/external/lit';
+import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { Editor, Placeholder, StarterKit, TextStyle } from '@umbraco-cms/backoffice/external/tiptap';
+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
+import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
+
+import './tiptap-fixed-menu.element.js';
+import './tiptap-hover-menu.element.js';
+
+const elementName = 'umb-input-tiptap';
+
+@customElement(elementName)
+export class UmbInputTiptapElement extends UmbFormControlMixin(UmbLitElement) {
+ readonly #requiredExtensions = [
+ StarterKit,
+ Placeholder.configure({
+ placeholder: ({ node }) => {
+ if (node.type.name === 'heading') {
+ return this.localize.term('placeholders_rteHeading');
+ }
+
+ return this.localize.term('placeholders_rteParagraph');
+ },
+ }),
+ TextStyle,
+ ];
+
+ @state()
+ private readonly _extensions: Array = [];
+
+ @property({ type: String })
+ override set value(value: string) {
+ this.#markup = value;
+
+ // Try to set the value to the editor if it is ready.
+ if (this._editor) {
+ this._editor.commands.setContent(value);
+ }
+ }
+ override get value() {
+ return this.#markup;
+ }
+
+ #markup = '';
+
+ @property({ attribute: false })
+ configuration?: UmbPropertyEditorConfigCollection;
+
+ /**
+ * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
+ */
+ @property({ type: Boolean, reflect: true })
+ readonly = false;
+
+ @state()
+ private _editor!: Editor;
+
+ @state()
+ _toolbar: UmbTiptapToolbarValue = [[[]]];
+
+ protected override async firstUpdated() {
+ await Promise.all([await this.#loadExtensions(), await this.#loadEditor()]);
+ }
+
+ async #loadExtensions() {
+ await new Promise((resolve) => {
+ this.observe(umbExtensionsRegistry.byType('tiptapExtension'), async (manifests) => {
+ const enabledExtensions = this.configuration?.getValueByAlias('extensions') ?? [];
+ for (const manifest of manifests) {
+ if (manifest.api) {
+ const extension = await loadManifestApi(manifest.api);
+ if (extension) {
+ // Check if the extension is enabled
+ if (enabledExtensions.includes(manifest.alias)) {
+ this._extensions.push(new extension(this));
+ }
+ }
+ }
+ }
+ resolve();
+ });
+ });
+ }
+
+ async #loadEditor() {
+ const element = this.shadowRoot?.querySelector('#editor');
+ if (!element) return;
+
+ const dimensions = this.configuration?.getValueByAlias<{ width?: number; height?: number }>('dimensions');
+ if (dimensions?.width) this.setAttribute('style', `max-width: ${dimensions.width}px;`);
+ if (dimensions?.height) element.setAttribute('style', `max-height: ${dimensions.height}px;`);
+
+ this._toolbar = this.configuration?.getValueByAlias('toolbar') ?? [[[]]];
+
+ const extensions = this._extensions
+ .map((ext) => ext.getTiptapExtensions({ configuration: this.configuration }))
+ .flat();
+
+ this._editor = new Editor({
+ element: element,
+ editable: !this.readonly,
+ extensions: [...this.#requiredExtensions, ...extensions],
+ content: this.#markup,
+ onBeforeCreate: ({ editor }) => {
+ this._extensions.forEach((ext) => ext.setEditor(editor));
+ },
+ onUpdate: ({ editor }) => {
+ this.#markup = editor.getHTML();
+ this.dispatchEvent(new UmbChangeEvent());
+ },
+ });
+ }
+
+ override render() {
+ return html`
+ ${when(
+ !this._editor && !this._extensions?.length,
+ () => html`
`,
+ () => html`
+
+ `,
+ )}
+
+ `;
+ }
+
+ static override readonly styles = [
+ css`
+ :host {
+ display: block;
+ position: relative;
+ z-index: 0;
+ }
+
+ :host([readonly]) {
+ pointer-events: none;
+
+ #editor {
+ background-color: var(--uui-color-surface-alt);
+ }
+ }
+
+ #loader {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .tiptap {
+ height: 100%;
+ width: 100%;
+ outline: none;
+ white-space: pre-wrap;
+ min-width: 0;
+ }
+
+ .tiptap .is-editor-empty:first-child::before {
+ color: var(--uui-color-text);
+ opacity: 0.55;
+ content: attr(data-placeholder);
+ float: left;
+ height: 0;
+ pointer-events: none;
+ }
+
+ #editor {
+ overflow: auto;
+ border-radius: var(--uui-border-radius);
+ border: 1px solid var(--uui-color-border);
+ padding: 1rem;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ border-top: 0;
+ box-sizing: border-box;
+ height: 100%;
+ width: 100%;
+ min-height: 400px;
+ display: grid; /* Don't ask me why this is needed, but it is. */
+ pre {
+ background-color: var(--uui-color-surface-alt);
+ padding: var(--uui-size-space-2) var(--uui-size-space-4);
+ border-radius: calc(var(--uui-border-radius) * 2);
+ overflow-x: auto;
+ }
+
+ code:not(pre > code) {
+ background-color: var(--uui-color-surface-alt);
+ padding: var(--uui-size-space-1) var(--uui-size-space-2);
+ border-radius: calc(var(--uui-border-radius) * 2);
+ }
+
+ code {
+ font-family: 'Roboto Mono', monospace;
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+ }
+
+ h1,
+ h2,
+ h3 {
+ margin-top: 0;
+ margin-bottom: 0.5em;
+ }
+
+ figure {
+ > p,
+ img {
+ pointer-events: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ &.ProseMirror-selectednode {
+ outline: 3px solid var(--uui-color-focus);
+ }
+ }
+
+ img {
+ &.ProseMirror-selectednode {
+ outline: 3px solid var(--uui-color-focus);
+ }
+ }
+
+ .umb-embed-holder {
+ display: inline-block;
+ position: relative;
+ }
+
+ .umb-embed-holder > * {
+ user-select: none;
+ pointer-events: none;
+ }
+
+ .umb-embed-holder.ProseMirror-selectednode {
+ outline: 2px solid var(--uui-palette-spanish-pink-light);
+ }
+
+ .umb-embed-holder::before {
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ content: ' ';
+ }
+
+ .umb-embed-holder.ProseMirror-selectednode::before {
+ background: rgba(0, 0, 0, 0.025);
+ }
+
+ /* Table-specific styling */
+ .tableWrapper {
+ margin: 1.5rem 0;
+ overflow-x: auto;
+
+ table {
+ border-collapse: collapse;
+ margin: 0;
+ overflow: hidden;
+ table-layout: fixed;
+ width: 100%;
+
+ td,
+ th {
+ border: 1px solid var(--uui-color-border);
+ box-sizing: border-box;
+ min-width: 1em;
+ padding: 6px 8px;
+ position: relative;
+ vertical-align: top;
+
+ > * {
+ margin-bottom: 0;
+ }
+ }
+
+ th {
+ background-color: var(--uui-color-background);
+ font-weight: bold;
+ text-align: left;
+ }
+
+ .selectedCell:after {
+ background: var(--uui-color-surface-emphasis);
+ content: '';
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ pointer-events: none;
+ position: absolute;
+ z-index: 2;
+ }
+
+ .column-resize-handle {
+ background-color: var(--uui-color-default);
+ bottom: -2px;
+ pointer-events: none;
+ position: absolute;
+ right: -2px;
+ top: 0;
+ width: 3px;
+ }
+ }
+
+ .resize-cursor {
+ cursor: ew-resize;
+ cursor: col-resize;
+ }
+ }
+ }
+ `,
+ ];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbInputTiptapElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-fixed-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-fixed-menu.element.ts
new file mode 100644
index 0000000000..37c837f0ce
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-fixed-menu.element.ts
@@ -0,0 +1,125 @@
+import type { UmbTiptapToolbarValue } from '../../extensions/types.js';
+import { css, customElement, html, map, property, state } from '@umbraco-cms/backoffice/external/lit';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
+
+import '../toolbar/tiptap-toolbar-dropdown-base.element.js';
+
+const elementName = 'umb-tiptap-fixed-menu';
+
+@customElement(elementName)
+export class UmbTiptapFixedMenuElement extends UmbLitElement {
+ #attached = false;
+ #extensionsController?: UmbExtensionsElementAndApiInitializer;
+
+ @state()
+ private _lookup?: Map;
+
+ @property({ type: Boolean, reflect: true })
+ readonly = false;
+
+ @property({ attribute: false })
+ editor?: Editor;
+
+ @property({ attribute: false })
+ configuration?: UmbPropertyEditorConfigCollection;
+
+ @property({ attribute: false })
+ toolbar: UmbTiptapToolbarValue = [[[]]];
+
+ override connectedCallback(): void {
+ super.connectedCallback();
+ this.#attached = true;
+ this.#observeExtensions();
+ }
+ override disconnectedCallback(): void {
+ this.#attached = false;
+ this.#extensionsController?.destroy();
+ this.#extensionsController = undefined;
+ super.disconnectedCallback();
+ }
+
+ #observeExtensions(): void {
+ if (!this.#attached) return;
+ this.#extensionsController?.destroy();
+
+ this.#extensionsController = new UmbExtensionsElementAndApiInitializer(
+ this,
+ umbExtensionsRegistry,
+ 'tiptapToolbarExtension',
+ [],
+ (manifest) => this.toolbar.flat(2).includes(manifest.alias),
+ (extensionControllers) => {
+ this._lookup = new Map(extensionControllers.map((ext) => [ext.alias, ext.component]));
+ },
+ );
+
+ this.#extensionsController.apiProperties = { configuration: this.configuration };
+ this.#extensionsController.elementProperties = { editor: this.editor, configuration: this.configuration };
+ }
+
+ override render() {
+ return html`${map(this.toolbar, (row, rowIndex) =>
+ map(
+ row,
+ (group, groupIndex) =>
+ html`${map(group, (alias, aliasIndex) => {
+ const newRow = rowIndex !== 0 && groupIndex === 0 && aliasIndex === 0;
+ return html`
+ ${this._lookup?.get(alias)}
+
`;
+ })}
+ `,
+ ),
+ )} `;
+ }
+
+ static override readonly styles = css`
+ :host([readonly]) {
+ pointer-events: none;
+ background-color: var(--uui-color-surface-alt);
+ }
+
+ :host {
+ border-radius: var(--uui-border-radius);
+ border: 1px solid var(--uui-color-border);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ background-color: var(--uui-color-surface);
+ color: var(--color-text);
+ display: grid;
+ grid-template-columns: repeat(auto-fill, 10px);
+ grid-auto-flow: row;
+ position: sticky;
+ top: -25px;
+ left: 0px;
+ right: 0px;
+ padding: var(--uui-size-space-3);
+ z-index: 9999999;
+ }
+
+ .item {
+ grid-column: span 3;
+ }
+
+ .separator {
+ background-color: var(--uui-color-border);
+ width: 1px;
+ place-self: center;
+ height: 22px;
+ }
+ .separator:last-child,
+ .separator:has(+ [data-new-row]) {
+ display: none;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbTiptapFixedMenuElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-hover-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-hover-menu.element.ts
new file mode 100644
index 0000000000..36a75174bd
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-hover-menu.element.ts
@@ -0,0 +1,53 @@
+import { LitElement, css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+@customElement('umb-tiptap-hover-menu')
+export class UmbTiptapHoverMenuElement extends LitElement {
+ @property({ attribute: false })
+ get editor() {
+ return this.#editor;
+ }
+ set editor(value) {
+ const oldValue = this.#editor;
+ if (value === oldValue) {
+ return;
+ }
+ this.#editor = value;
+ this.#editor?.on('selectionUpdate', this.#onUpdate);
+ this.#editor?.on('update', this.#onUpdate);
+ }
+ #editor?: Editor;
+
+ override connectedCallback(): void {
+ super.connectedCallback();
+ this.setAttribute('popover', '');
+ }
+
+ readonly #onUpdate = () => {
+ if (this.editor?.isActive('link')) {
+ // show the popover
+ this.showPopover();
+ } else {
+ this.requestUpdate();
+ }
+ };
+
+ override render() {
+ return html``;
+ }
+
+ static override readonly styles = css`
+ :host {
+ position: fixed;
+ background-color: var(--uui-color-surface-alt);
+ border: 1px solid var(--uui-color-border);
+ border-radius: var(--uui-size-border-radius);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-tiptap-hover-menu': UmbTiptapHoverMenuElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts
new file mode 100644
index 0000000000..a2311fa601
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts
@@ -0,0 +1,33 @@
+import { UmbTiptapToolbarButtonElement } from './tiptap-toolbar-button.element.js';
+import { customElement, html, ifDefined, when } from '@umbraco-cms/backoffice/external/lit';
+
+const elementName = 'umb-tiptap-toolbar-button-disabled';
+
+@customElement(elementName)
+export class UmbTiptapToolbarButtonDisabledElement extends UmbTiptapToolbarButtonElement {
+ override render() {
+ return html`
+ (this.api && this.editor ? this.api.execute(this.editor) : null)}>
+ ${when(
+ this.manifest?.meta.icon,
+ () => html``,
+ () => html`${this.manifest?.meta.label}`,
+ )}
+
+ `;
+ }
+}
+
+export { UmbTiptapToolbarButtonDisabledElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbTiptapToolbarButtonDisabledElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button.element.ts
new file mode 100644
index 0000000000..35212303be
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button.element.ts
@@ -0,0 +1,66 @@
+import type { ManifestTiptapToolbarExtensionButtonKind } from '../../extensions/index.js';
+import type { UmbTiptapToolbarElementApi } from '../../extensions/types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import { customElement, html, ifDefined, state, when } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+
+const elementName = 'umb-tiptap-toolbar-button';
+
+@customElement(elementName)
+export class UmbTiptapToolbarButtonElement extends UmbLitElement {
+ public api?: UmbTiptapToolbarElementApi;
+ public editor?: Editor;
+ public manifest?: ManifestTiptapToolbarExtensionButtonKind;
+
+ @state()
+ protected isActive = false;
+
+ override connectedCallback() {
+ super.connectedCallback();
+
+ if (this.editor) {
+ this.editor.on('selectionUpdate', this.#onEditorUpdate);
+ this.editor.on('update', this.#onEditorUpdate);
+ }
+ }
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+
+ if (this.editor) {
+ this.editor.off('selectionUpdate', this.#onEditorUpdate);
+ this.editor.off('update', this.#onEditorUpdate);
+ }
+ }
+
+ readonly #onEditorUpdate = () => {
+ if (this.api && this.editor && this.manifest) {
+ this.isActive = this.api.isActive(this.editor);
+ }
+ };
+
+ override render() {
+ return html`
+ (this.api && this.editor ? this.api.execute(this.editor) : null)}>
+ ${when(
+ this.manifest?.meta.icon,
+ () => html``,
+ () => html`${this.manifest?.meta.label}`,
+ )}
+
+ `;
+ }
+}
+
+export { UmbTiptapToolbarButtonElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbTiptapToolbarButtonElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts
new file mode 100644
index 0000000000..a5308a8539
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts
@@ -0,0 +1,120 @@
+import { css, html, nothing, repeat, type TemplateResult } from '@umbraco-cms/backoffice/external/lit';
+import type { PopoverContainerPlacement, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+
+export type TiptapDropdownItem = {
+ alias: string;
+ label: string;
+ nested?: TiptapDropdownItem[];
+ execute?: () => void;
+ isActive?: () => boolean;
+};
+
+export abstract class UmbTiptapToolbarDropdownBaseElement extends UmbLitElement {
+ protected abstract get items(): TiptapDropdownItem[];
+ protected abstract get label(): string;
+
+ readonly #onMouseEnter = (popoverId: string) => {
+ const popover = this.shadowRoot?.querySelector(`#${this.makeAlias(popoverId)}`) as UUIPopoverContainerElement;
+ if (!popover) return;
+ popover.showPopover();
+ };
+
+ readonly #onMouseLeave = (popoverId: string) => {
+ popoverId = popoverId.replace(/\s/g, '-').toLowerCase();
+ const popover = this.shadowRoot?.querySelector(`#${this.makeAlias(popoverId)}`) as UUIPopoverContainerElement;
+ if (!popover) return;
+ popover.hidePopover();
+ };
+
+ protected makeAlias(label: string) {
+ return label.replace(/\s/g, '-').toLowerCase();
+ }
+
+ protected renderItem(item: TiptapDropdownItem): TemplateResult {
+ return html`
+ this.#onMouseEnter(item.label)}
+ @mouseleave=${() => this.#onMouseLeave(item.label)}>
+
+ ${item.nested ? this.renderItems(item.label, item.nested) : nothing}
+
+ `;
+ }
+
+ protected renderItems(
+ label: string,
+ items: Array,
+ placement: PopoverContainerPlacement = 'right-start',
+ ): TemplateResult {
+ return html`
+
+ ${repeat(
+ items,
+ (item) => item.alias,
+ (item) => html`${this.renderItem(item)}`,
+ )}
+
+ `;
+ }
+ protected override render() {
+ return html`
+
+ ${this.renderItems(this.label, this.items, 'bottom-start')}
+ `;
+ }
+
+ static override readonly styles = [
+ UmbTextStyles,
+ css`
+ button {
+ border: unset;
+ background-color: unset;
+ font: unset;
+ text-align: unset;
+ }
+
+ uui-symbol-expand {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ }
+
+ .label {
+ border-radius: var(--uui-border-radius);
+ width: 100%;
+ box-sizing: border-box;
+ align-content: center;
+ padding: var(--uui-size-space-1) var(--uui-size-space-3);
+ padding-right: 21px;
+ align-items: center;
+ cursor: pointer;
+ color: var(--uui-color-text);
+ position: relative;
+ }
+
+ .label:hover {
+ background: var(--uui-color-surface-alt);
+ color: var(--uui-color-interactive-emphasis);
+ }
+
+ .selected-value {
+ background: var(--uui-color-surface-alt);
+ }
+
+ .popover-content {
+ background: var(--uui-color-surface);
+ border-radius: var(--uui-border-radius);
+ box-shadow: var(--uui-shadow-depth-3);
+ padding: var(--uui-size-space-1);
+ }
+ `,
+ ];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/embedded-media.extension.ts
new file mode 100644
index 0000000000..0e64a0153a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/embedded-media.extension.ts
@@ -0,0 +1,6 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { umbEmbeddedMedia } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapEmbeddedMediaExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [umbEmbeddedMedia.configure({ inline: true })];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/figure.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/figure.extension.ts
new file mode 100644
index 0000000000..1da6bdec35
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/figure.extension.ts
@@ -0,0 +1,6 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { Figure, Figcaption } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapFigureExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [Figcaption, Figure];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/image.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/image.extension.ts
new file mode 100644
index 0000000000..7254da3b61
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/image.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { UmbImage } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapImageExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions() {
+ return [UmbImage.configure({ inline: true })];
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/link.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/link.extension.ts
new file mode 100644
index 0000000000..4800000fda
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/link.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { UmbLink } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapLinkExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions() {
+ return [UmbLink.configure({ openOnClick: false })];
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/media-upload.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/media-upload.extension.ts
new file mode 100644
index 0000000000..1af085137c
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/media-upload.extension.ts
@@ -0,0 +1,128 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import type { UmbTiptapExtensionArgs } from '../types.js';
+import { imageSize } from '@umbraco-cms/backoffice/utils';
+import { Extension } from '@umbraco-cms/backoffice/external/tiptap';
+import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
+import { UmbId } from '@umbraco-cms/backoffice/id';
+import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
+import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
+import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
+
+export default class UmbTiptapMediaUploadExtensionApi extends UmbTiptapExtensionApiBase {
+ #configuration?: UmbPropertyEditorConfigCollection;
+
+ /**
+ * @returns {number} The maximum width of uploaded images
+ */
+ get maxWidth(): number {
+ const maxImageSize = parseInt(this.#configuration?.getValueByAlias('maxImageSize') ?? '', 10);
+ return isNaN(maxImageSize) ? 500 : maxImageSize;
+ }
+
+ /**
+ * @returns {Array} The allowed mime types for uploads
+ */
+ get allowedFileTypes(): string[] {
+ return (
+ this.#configuration?.getValueByAlias('allowedFileTypes') ?? ['image/jpeg', 'image/png', 'image/gif']
+ );
+ }
+
+ readonly #manager = new UmbTemporaryFileManager(this);
+ readonly #localize = new UmbLocalizationController(this);
+ #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
+
+ constructor(host: UmbControllerHost) {
+ super(host);
+ this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
+ this.#notificationContext = instance;
+ });
+ }
+
+ getTiptapExtensions(args: UmbTiptapExtensionArgs) {
+ this.#configuration = args?.configuration;
+
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const self = this;
+ return [
+ Extension.create({
+ name: 'umbMediaUpload',
+ onCreate() {
+ this.parent?.();
+ const host = this.editor.view.dom;
+
+ host.addEventListener('dragover', (event) => {
+ // Required to allow drop events
+ event.preventDefault();
+ });
+
+ host.addEventListener('drop', (event) => {
+ event.preventDefault();
+
+ const files = event.dataTransfer?.files;
+ if (!files) return;
+
+ self.#uploadTemporaryFile(files, this.editor);
+ });
+ },
+ }),
+ ];
+ }
+
+ /**
+ * Uploads the files to the server and inserts them into the editor as data URIs.
+ * The server will replace the data URI with a proper URL when the content is saved.
+ * @param {FileList} files The files to upload.
+ * @param {Editor} editor The editor to insert the images into.
+ */
+ async #uploadTemporaryFile(files: FileList, editor: Editor): Promise {
+ const filteredFiles = this.#filterFiles(files);
+ const fileModels = filteredFiles.map((file) => this.#mapFileToTemporaryFile(file));
+
+ this.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true, detail: fileModels }));
+
+ const uploads = await this.#manager.upload(fileModels);
+ const maxImageSize = this.maxWidth;
+
+ uploads.forEach(async (upload) => {
+ if (upload.status !== TemporaryFileStatus.SUCCESS) {
+ this.#notificationContext?.peek('danger', {
+ data: {
+ headline: upload.file.name,
+ message: this.#localize.term('errors_dissallowedMediaType'),
+ },
+ });
+ return;
+ }
+
+ const { width, height } = await imageSize(URL.createObjectURL(upload.file), { maxWidth: maxImageSize });
+
+ editor
+ .chain()
+ .focus()
+ .setImage({
+ src: URL.createObjectURL(upload.file),
+ width: width.toString(),
+ height: height.toString(),
+ 'data-tmpimg': upload.temporaryUnique,
+ })
+ .run();
+ });
+
+ this.dispatchEvent(new CustomEvent('rte.file.uploaded', { composed: true, bubbles: true, detail: uploads }));
+ }
+
+ #mapFileToTemporaryFile(file: File): UmbTemporaryFileModel {
+ return {
+ file,
+ temporaryUnique: UmbId.new(),
+ };
+ }
+
+ #filterFiles(files: FileList): File[] {
+ return Array.from(files).filter((file) => this.allowedFileTypes.includes(file.type));
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/subscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/subscript.extension.ts
new file mode 100644
index 0000000000..57bfb91472
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/subscript.extension.ts
@@ -0,0 +1,6 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { Subscript } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapBoldExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [Subscript];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/superscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/superscript.extension.ts
new file mode 100644
index 0000000000..453efb13e5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/superscript.extension.ts
@@ -0,0 +1,6 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { Superscript } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapBoldExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [Superscript];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/table.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/table.extension.ts
new file mode 100644
index 0000000000..d8c00b5d39
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/table.extension.ts
@@ -0,0 +1,6 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { Table, TableHeader, TableRow, TableCell } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapTableExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [Table.configure({ resizable: true }), TableHeader, TableRow, TableCell];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/text-align.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/text-align.extension.ts
new file mode 100644
index 0000000000..9855c4d150
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/text-align.extension.ts
@@ -0,0 +1,10 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { TextAlign } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapTextAlignExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [
+ TextAlign.configure({
+ types: ['heading', 'paragraph', 'blockquote', 'orderedList', 'bulletList', 'codeBlock'],
+ }),
+ ];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/underline.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/underline.extension.ts
new file mode 100644
index 0000000000..f9663f91c8
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/underline.extension.ts
@@ -0,0 +1,6 @@
+import { UmbTiptapExtensionApiBase } from '../types.js';
+import { Underline } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapUnderlineExtensionApi extends UmbTiptapExtensionApiBase {
+ getTiptapExtensions = () => [Underline];
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/index.ts
new file mode 100644
index 0000000000..8f09498433
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/index.ts
@@ -0,0 +1,3 @@
+export type * from './tiptap-extension.js';
+export type * from './tiptap-toolbar-extension.js';
+export * from './types.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts
new file mode 100644
index 0000000000..ed8f9f7f56
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts
@@ -0,0 +1,479 @@
+import type { ManifestTiptapExtension } from './tiptap-extension.js';
+import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar-extension.js';
+import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
+
+const kinds: Array = [
+ {
+ type: 'kind',
+ alias: 'Umb.Kind.Button',
+ matchKind: 'button',
+ matchType: 'tiptapToolbarExtension',
+ manifest: {
+ element: () => import('../components/toolbar/tiptap-toolbar-button.element.js'),
+ },
+ },
+];
+
+const coreExtensions: Array = [
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.Embed',
+ name: 'Embed Tiptap Extension',
+ api: () => import('./core/embedded-media.extension.js'),
+ meta: {
+ icon: 'icon-embed',
+ label: '#general_embed',
+ group: '#tiptap_extGroup_media',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.Link',
+ name: 'Link Tiptap Extension',
+ api: () => import('./core/link.extension.js'),
+ meta: {
+ icon: 'icon-link',
+ label: '#defaultdialogs_urlLinkPicker',
+ group: '#tiptap_extGroup_interactive',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.Figure',
+ name: 'Figure Tiptap Extension',
+ api: () => import('./core/figure.extension.js'),
+ meta: {
+ icon: 'icon-frame',
+ label: 'Figure',
+ group: '#tiptap_extGroup_media',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.Image',
+ name: 'Image Tiptap Extension',
+ api: () => import('./core/image.extension.js'),
+ meta: {
+ icon: 'icon-picture',
+ label: 'Image',
+ group: '#tiptap_extGroup_media',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Subscript',
+ name: 'Subscript Tiptap Extension',
+ api: () => import('./core/subscript.extension.js'),
+ meta: {
+ icon: 'icon-subscript',
+ label: 'Subscript',
+ group: '#tiptap_extGroup_formatting',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Superscript',
+ name: 'Superscript Tiptap Extension',
+ api: () => import('./core/superscript.extension.js'),
+ meta: {
+ icon: 'icon-superscript',
+ label: 'Superscript',
+ group: '#tiptap_extGroup_formatting',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Table',
+ name: 'Table Tiptap Extension',
+ api: () => import('./core/table.extension.js'),
+ meta: {
+ icon: 'icon-table',
+ label: 'Table',
+ group: '#tiptap_extGroup_interactive',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Underline',
+ name: 'Underline Tiptap Extension',
+ api: () => import('./core/underline.extension.js'),
+ meta: {
+ icon: 'icon-underline',
+ label: 'Underline',
+ group: '#tiptap_extGroup_formatting',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.TextAlign',
+ name: 'Text Align Tiptap Extension',
+ api: () => import('./core/text-align.extension.js'),
+ meta: {
+ icon: 'icon-text-align-justify',
+ label: 'Text Align',
+ group: '#tiptap_extGroup_formatting',
+ },
+ },
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.MediaUpload',
+ name: 'Media Upload Tiptap Extension',
+ api: () => import('./core/media-upload.extension.js'),
+ meta: {
+ icon: 'icon-image-up',
+ label: 'Media Upload',
+ group: '#tiptap_extGroup_media',
+ },
+ },
+];
+
+const toolbarExtensions: Array = [
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Blockquote',
+ name: 'Blockquote Tiptap Extension',
+ api: () => import('./toolbar/blockquote.extension.js'),
+ weight: 995,
+ meta: {
+ alias: 'blockquote',
+ icon: 'icon-blockquote',
+ label: 'Blockquote',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Bold',
+ name: 'Bold Tiptap Extension',
+ api: () => import('./toolbar/bold.extension.js'),
+ weight: 999,
+ meta: {
+ alias: 'bold',
+ icon: 'icon-bold',
+ label: 'Bold',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.CodeBlock',
+ name: 'Code Block Tiptap Extension',
+ api: () => import('./toolbar/code-block.extension.js'),
+ weight: 994,
+ meta: {
+ alias: 'codeBlock',
+ icon: 'icon-code',
+ label: 'Code Block',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.BulletList',
+ name: 'Bullet List Tiptap Extension',
+ api: () => import('./toolbar/bullet-list.extension.js'),
+ weight: 993,
+ meta: {
+ alias: 'bulletList',
+ icon: 'icon-bulleted-list',
+ label: 'Bullet List',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.OrderedList',
+ name: 'Ordered List Tiptap Extension',
+ api: () => import('./toolbar/ordered-list.extension.js'),
+ weight: 992,
+ meta: {
+ alias: 'orderedList',
+ icon: 'icon-ordered-list',
+ label: 'Ordered List',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Redo',
+ name: 'Redo Tiptap Extension',
+ api: () => import('./toolbar/redo.extension.js'),
+ element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'),
+ weight: 994,
+ meta: {
+ alias: 'redo',
+ icon: 'icon-redo',
+ label: 'Redo',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Strike',
+ name: 'Strike Tiptap Extension',
+ api: () => import('./toolbar/strike.extension.js'),
+ weight: 996,
+ meta: {
+ alias: 'strike',
+ icon: 'icon-strikethrough',
+ label: 'Strike',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Subscript',
+ name: 'Subscript Tiptap Extension',
+ api: () => import('./toolbar/subscript.extension.js'),
+ weight: 1010,
+ meta: {
+ alias: 'subscript',
+ icon: 'icon-subscript',
+ label: 'Subscript',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Superscript',
+ name: 'Superscript Tiptap Extension',
+ api: () => import('./toolbar/superscript.extension.js'),
+ weight: 1011,
+ meta: {
+ alias: 'superscript',
+ icon: 'icon-superscript',
+ label: 'Superscript',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Table',
+ name: 'Table Tiptap Extension',
+ api: () => import('./toolbar/table.extension.js'),
+ weight: 909,
+ meta: {
+ alias: 'table',
+ icon: 'icon-table',
+ label: 'Table',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Heading1',
+ name: 'Heading 1 Tiptap Extension',
+ api: () => import('./toolbar/heading1.extension.js'),
+ weight: 949,
+ meta: {
+ alias: 'heading1',
+ icon: 'icon-heading-1',
+ label: 'Heading 1',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Heading2',
+ name: 'Heading 2 Tiptap Extension',
+ api: () => import('./toolbar/heading2.extension.js'),
+ weight: 948,
+ meta: {
+ alias: 'heading2',
+ icon: 'icon-heading-2',
+ label: 'Heading 2',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Heading3',
+ name: 'Heading 3 Tiptap Extension',
+ api: () => import('./toolbar/heading3.extension.js'),
+ weight: 947,
+ meta: {
+ alias: 'heading3',
+ icon: 'icon-heading-3',
+ label: 'Heading 3',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.HorizontalRule',
+ name: 'Horizontal Rule Tiptap Extension',
+ api: () => import('./toolbar/horizontal-rule.extension.js'),
+ weight: 991,
+ meta: {
+ alias: 'horizontalRule',
+ icon: 'icon-horizontal-rule',
+ label: 'Horizontal Rule',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Italic',
+ name: 'Italic Tiptap Extension',
+ api: () => import('./toolbar/italic.extension.js'),
+ weight: 998,
+ meta: {
+ alias: 'italic',
+ icon: 'icon-italic',
+ label: 'Italic',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.TextAlignCenter',
+ name: 'Text Align Center Tiptap Extension',
+ api: () => import('./toolbar/text-align-center.extension.js'),
+ weight: 918,
+ meta: {
+ alias: 'text-align-center',
+ icon: 'icon-text-align-center',
+ label: 'Text Align Center',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.TextAlignJustify',
+ name: 'Text Align Justify Tiptap Extension',
+ api: () => import('./toolbar/text-align-justify.extension.js'),
+ weight: 916,
+ meta: {
+ alias: 'text-align-justify',
+ icon: 'icon-text-align-justify',
+ label: 'Text Align Justify',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.TextAlignLeft',
+ name: 'Text Align Left Tiptap Extension',
+ api: () => import('./toolbar/text-align-left.extension.js'),
+ weight: 919,
+ meta: {
+ alias: 'text-align-left',
+ icon: 'icon-text-align-left',
+ label: 'Text Align Left',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.TextAlignRight',
+ name: 'Text Align Right Tiptap Extension',
+ api: () => import('./toolbar/text-align-right.extension.js'),
+ weight: 917,
+ meta: {
+ alias: 'text-align-right',
+ icon: 'icon-text-align-right',
+ label: 'Text Align Right',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Underline',
+ name: 'Underline Tiptap Extension',
+ api: () => import('./toolbar/underline.extension.js'),
+ weight: 997,
+ meta: {
+ alias: 'underline',
+ icon: 'icon-underline',
+ label: 'Underline',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Undo',
+ name: 'Undo Tiptap Extension',
+ api: () => import('./toolbar/undo.extension.js'),
+ element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'),
+ weight: 994,
+ meta: {
+ alias: 'undo',
+ icon: 'icon-undo',
+ label: 'Undo',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Unlink',
+ name: 'Unlink Tiptap Extension',
+ api: () => import('./toolbar/unlink.extension.js'),
+ element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'),
+ weight: 101,
+ meta: {
+ alias: 'unlink',
+ icon: 'icon-unlink',
+ label: 'Unlink',
+ },
+ },
+];
+
+const umbToolbarExtensions: Array = [
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.SourceEditor',
+ name: 'Source Editor Tiptap Extension',
+ api: () => import('./toolbar/source-editor.extension.js'),
+ meta: {
+ alias: 'umbSourceEditor',
+ icon: 'icon-code-xml',
+ label: '#general_viewSourceCode',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.Link',
+ name: 'Link Tiptap Extension',
+ api: () => import('./toolbar/link.extension.js'),
+ meta: {
+ alias: 'umbLink',
+ icon: 'icon-link',
+ label: '#defaultdialogs_urlLinkPicker',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.MediaPicker',
+ name: 'Media Picker Tiptap Extension',
+ api: () => import('./toolbar/media-picker.extension.js'),
+ meta: {
+ alias: 'umbMedia',
+ icon: 'icon-picture',
+ label: 'Media picker',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.EmbeddedMedia',
+ name: 'Embedded Media Tiptap Extension',
+ api: () => import('./toolbar/embedded-media.extension.js'),
+ meta: {
+ alias: 'umbEmbeddedMedia',
+ icon: 'icon-embed',
+ label: '#general_embed',
+ },
+ },
+];
+
+const extensions = [...coreExtensions, ...toolbarExtensions, ...umbToolbarExtensions];
+
+export const manifests = [...kinds, ...extensions];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-extension.ts
new file mode 100644
index 0000000000..619792c6bb
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-extension.ts
@@ -0,0 +1,20 @@
+import type { UmbTiptapExtensionApi } from './types.js';
+import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api';
+
+export interface ManifestTiptapExtension
+ extends ManifestApi {
+ type: 'tiptapExtension';
+ meta: MetaType;
+}
+
+export interface MetaTiptapExtension {
+ icon: string;
+ label: string;
+ group: string;
+}
+
+declare global {
+ interface UmbExtensionManifestMap {
+ tiptapExtension: ManifestTiptapExtension;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-toolbar-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-toolbar-extension.ts
new file mode 100644
index 0000000000..a97d658328
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-toolbar-extension.ts
@@ -0,0 +1,29 @@
+import type { UmbTiptapToolbarElementApi } from './types.js';
+import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
+import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api';
+
+export interface ManifestTiptapToolbarExtension<
+ MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension,
+> extends ManifestElementAndApi {
+ type: 'tiptapToolbarExtension';
+ meta: MetaType;
+}
+
+export interface MetaTiptapToolbarExtension {
+ alias: string;
+ icon: string;
+ label: string;
+}
+
+export interface ManifestTiptapToolbarExtensionButtonKind<
+ MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension,
+> extends ManifestTiptapToolbarExtension {
+ type: 'tiptapToolbarExtension';
+ kind: 'button';
+}
+
+declare global {
+ interface UmbExtensionManifestMap {
+ tiptapToolbarExtension: ManifestTiptapToolbarExtension | ManifestTiptapToolbarExtensionButtonKind;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/blockquote.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/blockquote.extension.ts
new file mode 100644
index 0000000000..f061e7e6b6
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/blockquote.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarBlockquoteExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleBlockquote().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bold.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bold.extension.ts
new file mode 100644
index 0000000000..5ec1124963
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bold.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarBoldExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleBold().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bullet-list.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bullet-list.extension.ts
new file mode 100644
index 0000000000..983adb0c3d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bullet-list.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarBulletListExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleBulletList().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/code-block.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/code-block.extension.ts
new file mode 100644
index 0000000000..a3a30b0f5d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/code-block.extension.ts
@@ -0,0 +1,9 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarCodeBlockExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ // editor.chain().focus().toggleCode().run();
+ editor?.chain().focus().toggleCodeBlock().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/embedded-media.extension.ts
new file mode 100644
index 0000000000..b50872c1f0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/embedded-media.extension.ts
@@ -0,0 +1,42 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import { umbEmbeddedMedia } from '@umbraco-cms/backoffice/external/tiptap';
+import { UMB_EMBEDDED_MEDIA_MODAL } from '@umbraco-cms/backoffice/embedded-media';
+import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarEmbeddedMediaExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive = (editor: Editor) => editor.isActive(umbEmbeddedMedia.name) === true;
+
+ override async execute(editor?: Editor) {
+ const data = {
+ constrain: false,
+ height: 240,
+ width: 360,
+ url: '',
+ };
+
+ const attrs = editor?.getAttributes(umbEmbeddedMedia.name);
+ if (attrs) {
+ data.constrain = attrs['data-embed-constrain'];
+ data.height = attrs['data-embed-height'];
+ data.width = attrs['data-embed-width'];
+ data.url = attrs['data-embed-url'];
+ }
+
+ const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
+ const modalHandler = modalManager.open(this, UMB_EMBEDDED_MEDIA_MODAL, { data });
+
+ if (!modalHandler) return;
+
+ const result = await modalHandler.onSubmit().catch(() => undefined);
+ if (!result) return;
+
+ editor?.commands.setEmbeddedMedia({
+ markup: result.markup,
+ url: result.url,
+ constrain: result.constrain,
+ height: result.height?.toString(),
+ width: result.width?.toString(),
+ });
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading1.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading1.extension.ts
new file mode 100644
index 0000000000..b301f438e1
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading1.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarHeading1ExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive('heading', { level: 1 }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleHeading({ level: 1 }).run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading2.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading2.extension.ts
new file mode 100644
index 0000000000..663bc09d22
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading2.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarHeading2ExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive('heading', { level: 2 }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleHeading({ level: 2 }).run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading3.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading3.extension.ts
new file mode 100644
index 0000000000..3c3aed7cba
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading3.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarHeading3ExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive('heading', { level: 3 }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleHeading({ level: 3 }).run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/horizontal-rule.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/horizontal-rule.extension.ts
new file mode 100644
index 0000000000..58836d2257
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/horizontal-rule.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarHorizontalRuleExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().setHorizontalRule().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/italic.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/italic.extension.ts
new file mode 100644
index 0000000000..2ac2c77649
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/italic.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarItalicExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleItalic().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/link.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/link.extension.ts
new file mode 100644
index 0000000000..d78648f6fb
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/link.extension.ts
@@ -0,0 +1,140 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import { UmbLink } from '@umbraco-cms/backoffice/external/tiptap';
+import { UMB_LINK_PICKER_MODAL } from '@umbraco-cms/backoffice/multi-url-picker';
+import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbLinkPickerLink } from '@umbraco-cms/backoffice/multi-url-picker';
+import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
+
+export default class UmbTiptapToolbarLinkExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override async execute(editor?: Editor) {
+ const attrs = editor?.getAttributes(UmbLink.name) ?? {};
+ const link = this.#getLinkData(attrs);
+ const data = { config: {}, index: null };
+ const value = { link };
+
+ const overlaySize = this.configuration?.getValueByAlias('overlaySize') ?? 'small';
+
+ const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
+ const modalHandler = modalManager.open(this, UMB_LINK_PICKER_MODAL, { data, value, modal: { size: overlaySize } });
+
+ if (!modalHandler) return;
+
+ const result = await modalHandler.onSubmit().catch(() => undefined);
+ if (!result?.link) return;
+
+ const linkAttrs = this.#parseLinkData(result.link);
+
+ if (linkAttrs) {
+ editor?.chain().focus().extendMarkRange(UmbLink.name).setUmbLink(linkAttrs).run();
+ } else {
+ editor?.chain().focus().extendMarkRange(UmbLink.name).unsetLink().run();
+ }
+ }
+
+ #getLinkData(attrs: Record): UmbLinkPickerLink {
+ const queryString = attrs['data-anchor'];
+ const url = attrs.href?.substring(0, attrs.href.length - (queryString?.length ?? 0));
+ const unique = url?.includes('localLink:') ? url.substring(url.indexOf(':') + 1, url.indexOf('}')) : null;
+
+ return {
+ name: attrs.title,
+ queryString,
+ target: attrs.target,
+ type: attrs.type,
+ unique,
+ url,
+ };
+ }
+
+ #parseLinkData(link: UmbLinkPickerLink) {
+ const { name, target, type, unique } = link;
+ let { queryString, url } = link;
+
+ // If an anchor exists, check that it is appropriately prefixed
+ queryString = this.#queryStringFromUrl(queryString);
+
+ // The href might be an external url, so check the value for an anchor/querystring;
+ // `href` has the anchor re-appended later, hence the reset here to avoid duplicating the anchor
+ if (!queryString) {
+ const extractedInfo = this.#extractUrlAndQueryString(url, queryString);
+ url = extractedInfo.url;
+ queryString = extractedInfo.queryString;
+ }
+
+ // If we have a unique id, it must be a `/{localLink:guid}`
+ if (unique) {
+ url = `/{localLink:${unique}}`;
+ } else {
+ // If it's an email address and not `//user@domain.com` and protocol (e.g. mailto:, sip:) is not specified;
+ // then we'll assume it should be a "mailto" link.
+ url = this.#transformURLToMailto(url);
+
+ url = this.#ensureHttpProtocol(url);
+ }
+
+ const anchor = this.#getAnchorFromQueryString(queryString);
+
+ if (anchor) url += anchor;
+
+ if (!url) return null;
+
+ return {
+ type: type ?? 'external',
+ href: url,
+ 'data-anchor': anchor,
+ target,
+ title: name ?? url,
+ };
+ }
+
+ #extractUrlAndQueryString(url: string | null | undefined, queryString: string | null) {
+ const urlParts = url?.split(/([#?])/);
+ if (urlParts?.length === 3) {
+ url = urlParts[0];
+ queryString = urlParts[1] + urlParts[2];
+ }
+ return { url, queryString };
+ }
+
+ /**
+ * If the URL is prefixed "www.", then prepend "http://" protocol scheme.
+ */
+ #ensureHttpProtocol(url: string | null | undefined) {
+ if (!url) return null;
+ if (/^\s*www\./i.test(url)) {
+ url = `http://${url}`;
+ }
+ return url;
+ }
+
+ /**
+ * If the URL is an email address, then prepend "mailto:" protocol scheme.
+ */
+ #transformURLToMailto(url: string | null | undefined) {
+ if (!url) return null;
+ if (url?.includes('@') && !url.includes('//') && !url.includes(':')) {
+ url = `mailto:${url}`;
+ }
+ return url;
+ }
+
+ /**
+ * If the URL contains an anchor, then return the anchor.
+ */
+ #getAnchorFromQueryString(queryString: string | null) {
+ if (!queryString) return null;
+ return queryString.startsWith('#') || queryString.startsWith('?') ? queryString : null;
+ }
+
+ /**
+ * If the query string does not start with "?" or "#", then prepend it.
+ */
+ #queryStringFromUrl(queryString: string | null | undefined) {
+ if (!queryString) return null;
+ if (!queryString.startsWith('?') && !queryString.startsWith('#')) {
+ queryString = (queryString.startsWith('=') ? '#' : '?') + queryString;
+ }
+ return queryString;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/media-picker.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/media-picker.extension.ts
new file mode 100644
index 0000000000..0ba24357bd
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/media-picker.extension.ts
@@ -0,0 +1,135 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import { getGuidFromUdi, getProcessedImageUrl, imageSize } from '@umbraco-cms/backoffice/utils';
+import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
+import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media';
+import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import type { UmbMediaCaptionAltTextModalValue } from '@umbraco-cms/backoffice/media';
+
+export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbTiptapToolbarElementApiBase {
+ #modalManager?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
+
+ /**
+ * @returns {number} The maximum width of uploaded images
+ */
+ get maxWidth(): number {
+ const maxImageSize = parseInt(this.configuration?.getValueByAlias('maxImageSize') ?? '', 10);
+ return isNaN(maxImageSize) ? 500 : maxImageSize;
+ }
+
+ constructor(host: UmbControllerHost) {
+ super(host);
+
+ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
+ this.#modalManager = instance;
+ });
+ }
+
+ override isActive(editor?: Editor) {
+ return editor?.isActive('image') === true || editor?.isActive('figure') === true;
+ }
+
+ override async execute(editor: Editor) {
+ const currentTarget = editor.getAttributes('image');
+ const figure = editor.getAttributes('figure');
+
+ let currentMediaUdi: string | undefined = undefined;
+ if (currentTarget?.['data-udi']) {
+ currentMediaUdi = getGuidFromUdi(currentTarget['data-udi']);
+ }
+
+ let currentAltText: string | undefined = undefined;
+ if (currentTarget?.alt) {
+ currentAltText = currentTarget.alt;
+ }
+
+ let currentCaption: string | undefined = undefined;
+ if (figure?.figcaption) {
+ currentCaption = figure.figcaption;
+ }
+
+ const selection = await this.#openMediaPicker(currentMediaUdi);
+ if (!selection?.length) return;
+
+ const mediaGuid = selection[0];
+ const media = await this.#showMediaCaptionAltText(mediaGuid, currentAltText, currentCaption);
+ if (!media) return;
+
+ this.#insertInEditor(editor, mediaGuid, media);
+ }
+
+ async #openMediaPicker(currentMediaUdi?: string) {
+ const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, {
+ data: {
+ multiple: false,
+ //startNodeIsVirtual,
+ },
+ value: {
+ selection: currentMediaUdi ? [currentMediaUdi] : [],
+ },
+ });
+
+ if (!modalHandler) return;
+
+ const { selection } = await modalHandler.onSubmit().catch(() => ({ selection: undefined }));
+
+ return selection;
+ }
+
+ async #showMediaCaptionAltText(mediaUnique: string, altText?: string, caption?: string) {
+ const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, {
+ data: { mediaUnique },
+ value: {
+ url: '',
+ altText,
+ caption,
+ },
+ });
+ const mediaData = await modalHandler?.onSubmit().catch(() => null);
+ return mediaData;
+ }
+
+ async #insertInEditor(editor: Editor, mediaUnique: string, media: UmbMediaCaptionAltTextModalValue) {
+ if (!media?.url) return;
+
+ const { width, height } = await imageSize(media.url, { maxWidth: this.maxWidth });
+ const src = await getProcessedImageUrl(media.url, { width, height, mode: ImageCropModeModel.MAX });
+
+ const img = {
+ alt: media.altText,
+ src,
+ 'data-udi': `umb://media/${mediaUnique.replace(/-/g, '')}`,
+ width: width.toString(),
+ height: height.toString(),
+ };
+
+ if (media.caption) {
+ return editor.commands.insertContent({
+ type: 'figure',
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'image',
+ attrs: img,
+ },
+ ],
+ },
+ {
+ type: 'figcaption',
+ content: [
+ {
+ type: 'text',
+ text: media.caption,
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+ return editor.commands.setImage(img);
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/ordered-list.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/ordered-list.extension.ts
new file mode 100644
index 0000000000..f95d7da3d2
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/ordered-list.extension.ts
@@ -0,0 +1,11 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import { OrderedList, ListItem } from '@umbraco-cms/backoffice/external/tiptap';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarOrderedListExtensionApi extends UmbTiptapToolbarElementApiBase {
+ getTiptapExtensions = () => [OrderedList, ListItem];
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleOrderedList().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/redo.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/redo.extension.ts
new file mode 100644
index 0000000000..0aae301bba
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/redo.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarRedoExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor: Editor): boolean {
+ return editor.can().redo();
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().redo().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/source-editor.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/source-editor.extension.ts
new file mode 100644
index 0000000000..7ff4fcd4ca
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/source-editor.extension.ts
@@ -0,0 +1,26 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import { UMB_CODE_EDITOR_MODAL } from '@umbraco-cms/backoffice/code-editor';
+import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarSourceEditorExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override async execute(editor?: Editor) {
+ if (!editor) return;
+
+ const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
+ const modal = modalManager.open(this, UMB_CODE_EDITOR_MODAL, {
+ data: {
+ headline: 'Edit source code',
+ content: editor?.getHTML() ?? '',
+ language: 'html',
+ },
+ });
+
+ if (!modal) return;
+
+ const data = await modal.onSubmit().catch(() => undefined);
+ if (!data) return;
+
+ editor?.commands.setContent(data.content, true);
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/strike.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/strike.extension.ts
new file mode 100644
index 0000000000..1428e2009b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/strike.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarStrikeExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleStrike().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/style-select.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/style-select.extension.ts
new file mode 100644
index 0000000000..96868aa349
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/style-select.extension.ts
@@ -0,0 +1,24 @@
+import { UmbTiptapToolbarDropdownBaseElement, type TiptapDropdownItem } from '../../components/index.js';
+import { customElement, state } from '@umbraco-cms/backoffice/external/lit';
+
+const elementName = 'umb-tiptap-style-select-toolbar-element';
+
+@customElement(elementName)
+export class UmbTiptapToolbarStyleSelectToolbarElement extends UmbTiptapToolbarDropdownBaseElement {
+ protected override label = 'Style select';
+
+ @state()
+ protected override get items(): TiptapDropdownItem[] {
+ throw new Error('Method not implemented.');
+ }
+
+ static override readonly styles = UmbTiptapToolbarDropdownBaseElement.styles;
+}
+
+export { UmbTiptapToolbarStyleSelectToolbarElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbTiptapToolbarStyleSelectToolbarElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/subscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/subscript.extension.ts
new file mode 100644
index 0000000000..121e5210a6
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/subscript.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarSubscriptExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleSubscript().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/superscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/superscript.extension.ts
new file mode 100644
index 0000000000..d46e966d29
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/superscript.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarSuperscriptExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleSuperscript().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/table.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/table.extension.ts
new file mode 100644
index 0000000000..3ef38f6f13
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/table.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarTableExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true });
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-center.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-center.extension.ts
new file mode 100644
index 0000000000..a027bc18c1
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-center.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarTextAlignCenterExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive({ textAlign: 'center' }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().setTextAlign('center').run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-justify.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-justify.extension.ts
new file mode 100644
index 0000000000..38a53fc008
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-justify.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarTextAlignJustifyExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive({ textAlign: 'justify' }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().setTextAlign('justify').run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-left.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-left.extension.ts
new file mode 100644
index 0000000000..86b90b7e81
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-left.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarTextAlignLeftExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive({ textAlign: 'left' }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().setTextAlign('left').run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-right.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-right.extension.ts
new file mode 100644
index 0000000000..8f3984bb5c
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-right.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarTextAlignRightExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor?: Editor) {
+ return editor?.isActive({ textAlign: 'right' }) === true;
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().setTextAlign('right').run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/underline.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/underline.extension.ts
new file mode 100644
index 0000000000..9a2aab8b50
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/underline.extension.ts
@@ -0,0 +1,8 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarUnderlineExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override execute(editor?: Editor) {
+ editor?.chain().focus().toggleUnderline().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/undo.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/undo.extension.ts
new file mode 100644
index 0000000000..1cc600442b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/undo.extension.ts
@@ -0,0 +1,12 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarUndoExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive(editor: Editor): boolean {
+ return editor.can().undo();
+ }
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().undo().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/unlink.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/unlink.extension.ts
new file mode 100644
index 0000000000..a3137f6ec5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/unlink.extension.ts
@@ -0,0 +1,10 @@
+import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+
+export default class UmbTiptapToolbarUnlinkExtensionApi extends UmbTiptapToolbarElementApiBase {
+ override isActive = (editor?: Editor) => editor?.isActive('umbLink') ?? false;
+
+ override execute(editor?: Editor) {
+ editor?.chain().focus().unsetUmbLink().run();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts
new file mode 100644
index 0000000000..927d22cf63
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts
@@ -0,0 +1,102 @@
+import type { ManifestTiptapExtension } from './tiptap-extension.js';
+import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar-extension.js';
+import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
+import type { Editor, Extension, Mark, Node } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
+import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
+
+export interface UmbTiptapExtensionApi extends UmbApi {
+ /**
+ * The manifest for the extension.
+ */
+ manifest?: ManifestTiptapExtension;
+
+ /**
+ * Sets the editor instance to the extension.
+ */
+ setEditor(editor: Editor): void;
+
+ /**
+ * Gets the Tiptap extensions for the editor.
+ */
+ getTiptapExtensions(args?: UmbTiptapExtensionArgs): Array;
+}
+
+export abstract class UmbTiptapExtensionApiBase extends UmbControllerBase implements UmbTiptapExtensionApi {
+ /**
+ * The manifest for the extension.
+ */
+ manifest?: ManifestTiptapExtension;
+
+ /**
+ * The editor instance.
+ */
+ protected _editor?: Editor;
+
+ /**
+ * @inheritdoc
+ */
+ setEditor(editor: Editor): void {
+ this._editor = editor;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ abstract getTiptapExtensions(args?: UmbTiptapExtensionArgs): Array;
+}
+
+export interface UmbTiptapExtensionArgs {
+ /**
+ * The data type configuration for the property editor that the editor is used for.
+ * You can populate this manually if you are using the editor outside of a property editor with the {@link UmbPropertyEditorConfigCollection} object.
+ * @remark This is only available when the editor is used in a property editor or populated manually.
+ */
+ configuration?: UmbPropertyEditorConfigCollection;
+}
+
+export interface UmbTiptapToolbarElementApi extends UmbApi, UmbTiptapExtensionArgs {
+ /**
+ * The manifest for the extension.
+ */
+ manifest?: ManifestTiptapToolbarExtension;
+
+ /**
+ * Executes the toolbar element action.
+ */
+ execute(editor: Editor): void;
+
+ /**
+ * Checks if the toolbar element is active.
+ */
+ isActive(editor: Editor): boolean;
+}
+
+export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase implements UmbTiptapToolbarElementApi {
+ /**
+ * The manifest for the extension.
+ */
+ manifest?: ManifestTiptapToolbarExtension;
+
+ /**
+ * The data type configuration for the property editor that the editor is used for.
+ */
+ configuration?: UmbPropertyEditorConfigCollection;
+
+ /**
+ * A method to execute the toolbar element action.
+ */
+ public abstract execute(editor: Editor): void;
+
+ /**
+ * Informs the toolbar element if it is active or not. It uses the manifest meta alias to check if the toolbar element is active.
+ * @see {ManifestTiptapToolbarExtension}
+ * @param {Editor} editor The editor instance.
+ * @returns {boolean} Returns true if the toolbar element is active.
+ */
+ public isActive(editor: Editor) {
+ return editor && this.manifest?.meta.alias ? editor?.isActive(this.manifest.meta.alias) : false;
+ }
+}
+
+export type UmbTiptapToolbarValue = Array>>;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/index.ts
new file mode 100644
index 0000000000..f0f1ade33d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/index.ts
@@ -0,0 +1,2 @@
+export * from './components/index.js';
+export * from './extensions/index.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/manifests.ts
new file mode 100644
index 0000000000..aea7de21b6
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/manifests.ts
@@ -0,0 +1,5 @@
+import { manifests as extensions } from './extensions/manifests.js';
+import { manifests as propertyEditors } from './property-editors/manifests.js';
+import { manifests as plugins } from './plugins/manifests.js';
+
+export const manifests = [...extensions, ...propertyEditors, ...plugins];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block-picker-toolbar.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block-picker-toolbar.extension.ts
new file mode 100644
index 0000000000..1777a677c8
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block-picker-toolbar.extension.ts
@@ -0,0 +1,55 @@
+import { UMB_BLOCK_RTE_MANAGER_CONTEXT, UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '@umbraco-cms/backoffice/block-rte';
+import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
+import { UmbTiptapToolbarElementApiBase } from '@umbraco-cms/backoffice/tiptap';
+import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+
+export default class UmbTiptapBlockPickerToolbarExtension extends UmbTiptapToolbarElementApiBase {
+ #blocks?: Array;
+ #entriesContext?: typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE;
+
+ constructor(host: UmbControllerHost) {
+ super(host);
+
+ this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => {
+ this.observe(
+ context.blockTypes,
+ (blockTypes) => {
+ this.#blocks = blockTypes;
+ },
+ 'blockType',
+ );
+ });
+ this.consumeContext(UMB_BLOCK_RTE_ENTRIES_CONTEXT, (context) => {
+ this.#entriesContext = context;
+ });
+ }
+
+ override isActive(editor: Editor) {
+ return editor.isActive('umbRteBlock') || editor.isActive('umbRteBlockInline');
+ }
+
+ override async execute() {
+ return this.#createBlock();
+ }
+
+ #createBlock() {
+ if (!this.#entriesContext) {
+ console.error('[Block Picker] No entries context available.');
+ return;
+ }
+
+ let createPath: string | undefined = undefined;
+
+ if (this.#blocks?.length === 1) {
+ const elementKey = this.#blocks[0].contentElementTypeKey;
+ createPath = this.#entriesContext.getPathForCreateBlock() + 'modal/umb-modal-workspace/create/' + elementKey;
+ } else {
+ createPath = this.#entriesContext.getPathForCreateBlock();
+ }
+
+ if (createPath) {
+ window.history.pushState({}, '', createPath);
+ }
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block.extension.ts
new file mode 100644
index 0000000000..38e82123f5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block.extension.ts
@@ -0,0 +1,130 @@
+import { UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '../../types.js';
+import { UmbTiptapExtensionApiBase } from '@umbraco-cms/backoffice/tiptap';
+import { Node } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import { distinctUntilChanged } from '@umbraco-cms/backoffice/external/rxjs';
+import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block';
+import { UMB_BLOCK_RTE_MANAGER_CONTEXT, type UmbBlockRteLayoutModel } from '@umbraco-cms/backoffice/block-rte';
+
+declare module '@tiptap/core' {
+ interface Commands {
+ umbRteBlock: {
+ setBlock: (options: { contentKey: string }) => ReturnType;
+ };
+ umbRteBlockInline: {
+ setBlockInline: (options: { contentKey: string }) => ReturnType;
+ };
+ }
+}
+
+const umbRteBlock = Node.create({
+ name: 'umbRteBlock',
+ group: 'block',
+ content: undefined, // The block does not have any content, it is just a wrapper.
+ atom: true, // The block is an atom, meaning it is a single unit that cannot be split.
+ marks: '', // We do not allow marks on the block
+ draggable: true,
+ selectable: true,
+
+ addAttributes() {
+ return {
+ [UMB_BLOCK_RTE_DATA_CONTENT_KEY]: {
+ isRequired: true,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'umb-rte-block' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['umb-rte-block', HTMLAttributes];
+ },
+
+ addCommands() {
+ return {
+ setBlock:
+ (options) =>
+ ({ commands }) => {
+ const attrs = { [UMB_BLOCK_RTE_DATA_CONTENT_KEY]: options.contentKey };
+ return commands.insertContent({
+ type: this.name,
+ attrs,
+ });
+ },
+ };
+ },
+});
+
+const umbRteBlockInline = umbRteBlock.extend({
+ name: 'umbRteBlockInline',
+ group: 'inline',
+ inline: true,
+
+ parseHTML() {
+ return [{ tag: 'umb-rte-block-inline' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['umb-rte-block-inline', HTMLAttributes];
+ },
+
+ addCommands() {
+ return {
+ setBlockInline:
+ (options) =>
+ ({ commands }) => {
+ const attrs = { [UMB_BLOCK_RTE_DATA_CONTENT_KEY]: options.contentKey };
+ return commands.insertContent({
+ type: this.name,
+ attrs,
+ });
+ },
+ };
+ },
+});
+
+export default class UmbTiptapBlockElementApi extends UmbTiptapExtensionApiBase {
+ constructor(host: UmbControllerHost) {
+ super(host);
+
+ this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => {
+ this.observe(
+ context.contents.pipe(
+ distinctUntilChanged((prev, curr) => prev.map((y) => y.key).join() === curr.map((y) => y.key).join()),
+ ),
+ (contents) => {
+ this.#updateBlocks(contents, context.getLayouts());
+ },
+ 'contents',
+ );
+ });
+ }
+
+ getTiptapExtensions() {
+ return [umbRteBlock, umbRteBlockInline];
+ }
+
+ #updateBlocks(blocks: UmbBlockDataModel[], layouts: Array) {
+ const editor = this._editor;
+ if (!editor) return;
+
+ const existingBlocks = Array.from(editor.view.dom.querySelectorAll('umb-rte-block, umb-rte-block-inline')).map(
+ (x) => x.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY),
+ );
+ const newBlocks = blocks.filter((x) => !existingBlocks.find((contentKey) => contentKey === x.key));
+
+ newBlocks.forEach((block) => {
+ // Find layout for block
+ const layout = layouts.find((x) => x.contentKey === block.key);
+ const inline = layout?.displayInline ?? false;
+
+ if (inline) {
+ editor.commands.setBlockInline({ contentKey: block.key });
+ } else {
+ editor.commands.setBlock({ contentKey: block.key });
+ }
+ });
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/manifests.ts
new file mode 100644
index 0000000000..aafd570b89
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/manifests.ts
@@ -0,0 +1,29 @@
+import type { ManifestTiptapExtension } from '../extensions/tiptap-extension.js';
+import type { ManifestTiptapToolbarExtensionButtonKind } from '../extensions/tiptap-toolbar-extension.js';
+
+export const manifests: Array = [
+ {
+ type: 'tiptapExtension',
+ alias: 'Umb.Tiptap.Block',
+ name: 'Block Tiptap Extension',
+ api: () => import('./block.extension.js'),
+ meta: {
+ icon: 'icon-plugin',
+ label: 'Block',
+ group: '#tiptap_extGroup_interactive',
+ },
+ },
+ {
+ type: 'tiptapToolbarExtension',
+ kind: 'button',
+ alias: 'Umb.Tiptap.Toolbar.BlockPicker',
+ name: 'Block Picker Tiptap Extension Button',
+ api: () => import('./block-picker-toolbar.extension.js'),
+ weight: 90,
+ meta: {
+ alias: 'umbblockpicker',
+ icon: 'icon-plugin',
+ label: '#blockEditor_insertBlock',
+ },
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/manifests.ts
new file mode 100644
index 0000000000..734619f0cf
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/manifests.ts
@@ -0,0 +1,5 @@
+// eslint-disable-next-line local-rules/no-relative-import-to-import-map-module
+import { manifests as tiptapManifests } from './tiptap/manifests.js';
+import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';
+
+export const manifests: Array = [...tiptapManifests];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts
new file mode 100644
index 0000000000..d020ec400d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts
@@ -0,0 +1,210 @@
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
+import { customElement, css, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import {
+ UmbPropertyValueChangeEvent,
+ type UmbPropertyEditorConfigCollection,
+ type UmbPropertyEditorUiElement,
+} from '@umbraco-cms/backoffice/property-editor';
+
+type UmbTiptapExtensionConfig = {
+ alias: string;
+ label: string;
+ icon?: string;
+ group: string;
+};
+
+type UmbTiptapExtensionGroupItem = {
+ alias: string;
+ label: string;
+ icon?: string;
+ selected: boolean;
+};
+
+type UmbTiptapExtensionGroup = {
+ group: string;
+ extensions: UmbTiptapExtensionGroupItem[];
+};
+
+const elementName = 'umb-property-editor-ui-tiptap-extensions-configuration';
+
+@customElement(elementName)
+export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
+ extends UmbLitElement
+ implements UmbPropertyEditorUiElement
+{
+ @property({ attribute: false })
+ value?: Array = [];
+
+ @property({ attribute: false })
+ config?: UmbPropertyEditorConfigCollection;
+
+ @state()
+ private _extensionCategories: UmbTiptapExtensionGroup[] = [];
+
+ @state()
+ private _extensionConfigs: UmbTiptapExtensionConfig[] = [];
+
+ protected override async firstUpdated(_changedProperties: PropertyValueMap) {
+ super.firstUpdated(_changedProperties);
+
+ this.observe(umbExtensionsRegistry.byType('tiptapExtension'), (extensions) => {
+ this._extensionConfigs = extensions
+ .sort((a, b) => a.alias.localeCompare(b.alias))
+ .map((ext) => {
+ return {
+ alias: ext.alias,
+ label: ext.meta.label,
+ icon: ext.meta.icon,
+ group: ext.meta.group,
+ };
+ });
+
+ if (!this.value) {
+ // The default value is all extensions enabled
+ this.value = this._extensionConfigs.map((ext) => ext.alias);
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ this.#setupExtensionCategories();
+ });
+ }
+
+ #setupExtensionCategories() {
+ const useDefault = !this.value; // The default value is all extensions enabled
+ const withSelectedProperty = this._extensionConfigs.map((extensionConfig) => {
+ return {
+ ...extensionConfig,
+ selected: useDefault ? true : this.value!.includes(extensionConfig.alias),
+ };
+ });
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ const grouped = Object.groupBy(
+ withSelectedProperty,
+ (item: UmbTiptapExtensionConfig) => item.group || 'Uncategorized',
+ );
+
+ this._extensionCategories = Object.keys(grouped)
+ .sort((a, b) => a.localeCompare(b))
+ .map((key) => ({
+ group: key,
+ extensions: grouped[key],
+ }));
+ }
+
+ #onExtensionClick(item: UmbTiptapExtensionGroupItem) {
+ item.selected = !item.selected;
+
+ if (!this.value) {
+ this.value = [];
+ }
+
+ if (item.selected) {
+ this.value = [...this.value, item.alias];
+ } else {
+ this.value = this.value.filter((alias) => alias !== item.alias);
+ }
+
+ this.requestUpdate('_extensionCategories');
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ override render() {
+ return html`
+
+ ${repeat(
+ this._extensionCategories,
+ (group) => html`
+
+
${this.localize.string(group.group)}
+ ${repeat(
+ group.extensions,
+ (item) => html`
+
+ this.#onExtensionClick(item)}>
+
+
+ this.#onExtensionClick(item)}>
+
+ `,
+ )}
+
+ `,
+ )}
+
+ `;
+ }
+
+ static override readonly styles = [
+ UmbTextStyles,
+ css`
+ uui-icon {
+ width: unset;
+ height: unset;
+ display: flex;
+ vertical-align: unset;
+ }
+
+ uui-button.selected {
+ --uui-button-border-color: var(--uui-color-selected);
+ --uui-button-border-width: 2px;
+ }
+
+ .extensions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-top: 16px;
+ }
+
+ .extension-item {
+ display: grid;
+ grid-template-columns: 36px 1fr;
+ grid-template-rows: 1fr;
+ align-items: center;
+ gap: 9px;
+ }
+
+ .group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 12px;
+ background-color: var(--uui-color-surface-alt);
+ border: 1px solid var(--uui-color-border);
+ border-radius: 6px;
+ }
+
+ .group-name {
+ grid-column: 1 / -1;
+ display: flex;
+ font-weight: bold;
+ margin: 0;
+ }
+ `,
+ ];
+}
+
+export { UmbPropertyEditorUiTiptapExtensionsConfigurationElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPropertyEditorUiTiptapExtensionsConfigurationElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts
new file mode 100644
index 0000000000..74d387c06a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts
@@ -0,0 +1,329 @@
+import type { UmbTiptapToolbarValue } from '../../../extensions/types.js';
+import { customElement, css, html, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+import { UmbPropertyValueChangeEvent, type UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
+import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
+
+type UmbTiptapToolbarExtension = {
+ alias: string;
+ label: string;
+ icon: string;
+};
+const elementName = 'umb-property-editor-ui-tiptap-toolbar-configuration';
+
+@customElement(elementName)
+export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
+ extends UmbLitElement
+ implements UmbPropertyEditorUiElement
+{
+ readonly #inUse: Set = new Set();
+
+ #currentDragItem?: {
+ alias: string;
+ fromPos?: [number, number, number];
+ };
+
+ #lookup?: Map;
+
+ @state()
+ private _extensions: Array = [];
+
+ @property({ attribute: false })
+ set value(value: UmbTiptapToolbarValue | undefined) {
+ if (!value) {
+ this.#value = [[[]]];
+ } else {
+ // TODO: This can be optimized with cashing;
+ this.#value = value ? value.map((rows) => rows.map((groups) => [...groups])) : [[[]]];
+ value.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#inUse.add(alias))));
+ }
+ }
+ get value(): UmbTiptapToolbarValue {
+ // TODO: This can be optimized with cashing;
+ return this.#value.map((rows) => rows.map((groups) => [...groups]));
+ }
+ #value: UmbTiptapToolbarValue = [[[]]];
+
+ protected override async firstUpdated(_changedProperties: PropertyValueMap) {
+ super.firstUpdated(_changedProperties);
+
+ this.observe(umbExtensionsRegistry.byType('tiptapToolbarExtension'), (extensions) => {
+ this._extensions = extensions.map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon }));
+ this.#lookup = new Map(this._extensions.map((ext) => [ext.alias, ext]));
+ });
+ }
+
+ #onDragStart(event: DragEvent, alias: string, fromPos?: [number, number, number]) {
+ event.dataTransfer!.effectAllowed = 'move';
+ this.#currentDragItem = { alias, fromPos };
+ }
+
+ #onDragOver(event: DragEvent) {
+ event.preventDefault();
+ event.dataTransfer!.dropEffect = 'move';
+ }
+
+ #onDragEnd(event: DragEvent) {
+ event.preventDefault();
+ if (event.dataTransfer?.dropEffect === 'none') {
+ const { fromPos } = this.#currentDragItem ?? {};
+ if (!fromPos) return;
+
+ this.#removeItem(fromPos);
+ }
+ }
+
+ #onDrop(event: DragEvent, toPos?: [number, number, number]) {
+ event.preventDefault();
+ const { alias, fromPos } = this.#currentDragItem ?? {};
+
+ // Remove item if no destination position is provided
+ if (fromPos && !toPos) {
+ this.#removeItem(fromPos);
+ return;
+ }
+ // Move item if both source and destination positions are available
+ if (fromPos && toPos) {
+ this.#moveItem(fromPos, toPos);
+ return;
+ }
+ // Insert item if an alias and a destination position are provided
+ if (alias && toPos) {
+ this.#insertItem(alias, toPos);
+ }
+ }
+
+ #moveItem(from: [number, number, number], to: [number, number, number]) {
+ const [rowIndex, groupIndex, itemIndex] = from;
+
+ // Get the item to move from the 'from' position
+ const itemToMove = this.#value[rowIndex][groupIndex][itemIndex];
+
+ // Remove the item from the original position
+ this.#value[rowIndex][groupIndex].splice(itemIndex, 1);
+
+ this.#insertItem(itemToMove, to);
+ }
+
+ #insertItem(alias: string, toPos: [number, number, number]) {
+ const [rowIndex, groupIndex, itemIndex] = toPos;
+
+ // Insert the item into the new position
+ const inserted = this.#value[rowIndex][groupIndex].splice(itemIndex, 0, alias);
+ inserted.forEach((alias) => this.#inUse.add(alias));
+
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ #removeItem(from: [number, number, number]) {
+ const [rowIndex, groupIndex, itemIndex] = from;
+
+ const removed = this.#value[rowIndex][groupIndex].splice(itemIndex, 1);
+ removed.forEach((alias) => this.#inUse.delete(alias));
+
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ #addGroup(rowIndex: number, groupIndex: number) {
+ this.#value[rowIndex].splice(groupIndex, 0, []);
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ #removeGroup(rowIndex: number, groupIndex: number) {
+ if (this.#value[rowIndex].length > groupIndex) {
+ const removed = this.#value[rowIndex].splice(groupIndex, 1);
+ removed.forEach((group) => group.forEach((alias) => this.#inUse.delete(alias)));
+ }
+
+ // Prevent leaving an empty group
+ if (this.#value[rowIndex].length === 0) {
+ this.#value[rowIndex][groupIndex] = [];
+ }
+
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ #addRow(rowIndex: number) {
+ this.#value.splice(rowIndex, 0, [[]]);
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ #removeRow(rowIndex: number) {
+ if (this.#value.length > rowIndex) {
+ const removed = this.#value.splice(rowIndex, 1);
+ removed.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#inUse.delete(alias))));
+ }
+
+ // Prevent leaving an empty row
+ if (this.#value.length === 0) {
+ this.#value[rowIndex] = [[]];
+ }
+
+ this.dispatchEvent(new UmbPropertyValueChangeEvent());
+ }
+
+ override render() {
+ return html`
+ ${repeat(this.#value, (row, rowIndex) => this.#renderRow(row, rowIndex))}
+ this.#addRow(this.#value.length)}>
+
+ Add row
+
+ ${this.#renderExtensions()}
+ `;
+ }
+
+ #renderRow(row: string[][], rowIndex: number) {
+ return html`
+
+ ${repeat(row, (group, groupIndex) => this.#renderGroup(group, rowIndex, groupIndex))}
+ this.#addGroup(rowIndex, row.length)}>
+
+ Add group
+
+ this.#removeRow(rowIndex)}>
+
+
+
+ `;
+ }
+
+ #renderGroup(group: string[], rowIndex: number, groupIndex: number) {
+ return html`
+ this.#onDrop(e, [rowIndex, groupIndex, group.length])}>
+ ${group.map((alias, itemIndex) => this.#renderItem(alias, rowIndex, groupIndex, itemIndex))}
+ this.#removeGroup(rowIndex, groupIndex)}>
+
+
+
+ `;
+ }
+
+ #renderItem(alias: string, rowIndex: number, groupIndex: number, itemIndex: number) {
+ const extension = this.#lookup?.get(alias);
+ if (!extension) return nothing;
+ return html`
+ this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
+
+
+ `;
+ }
+
+ #renderExtensions() {
+ return html`
+
+ ${repeat(
+ this._extensions.filter((ext) => !this.#inUse.has(ext.alias)),
+ (extension) => html`
+
this.#onDragStart(e, extension.alias)}
+ @dragend=${this.#onDragEnd}>
+
+
+ `,
+ )}
+
+ `;
+ }
+
+ static override readonly styles = [
+ UmbTextStyles,
+ css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+ .extensions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ border-radius: var(--uui-border-radius);
+ background-color: var(--uui-color-surface-alt);
+ padding: 6px;
+ min-height: 30px;
+ min-width: 30px;
+ }
+ .row {
+ position: relative;
+ display: flex;
+ gap: 12px;
+ }
+ .group {
+ position: relative;
+ display: flex;
+ gap: 3px;
+ border-radius: var(--uui-border-radius);
+ background-color: var(--uui-color-surface-alt);
+ padding: 6px;
+ min-height: 32px;
+ min-width: 32px;
+ }
+ .item {
+ padding: var(--uui-size-space-2);
+ border: 1px solid var(--uui-color-border);
+ border-radius: var(--uui-border-radius);
+ background-color: var(--uui-color-surface);
+ cursor: move;
+ display: flex;
+ box-sizing: border-box;
+ width: 32px;
+ height: 32px;
+ justify-content: center;
+ }
+
+ .remove-row-button,
+ .remove-group-button {
+ display: none;
+ }
+ .remove-group-button {
+ position: absolute;
+ top: -26px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 1;
+ }
+
+ .row:hover .remove-row-button:not(.hidden),
+ .group:hover .remove-group-button:not(.hidden) {
+ display: flex;
+ }
+ umb-icon {
+ /* Prevents titles from bugging out */
+ pointer-events: none;
+ }
+ `,
+ ];
+}
+
+export { UmbPropertyEditorUiTiptapToolbarConfigurationElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPropertyEditorUiTiptapToolbarConfigurationElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/manifests.ts
new file mode 100644
index 0000000000..a2791c4935
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/manifests.ts
@@ -0,0 +1,99 @@
+import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';
+
+export const manifests: Array = [
+ {
+ type: 'propertyEditorUi',
+ alias: 'Umb.PropertyEditorUi.Tiptap',
+ name: 'Rich Text Editor [Tiptap] Property Editor UI',
+ element: () => import('./property-editor-ui-tiptap.element.js'),
+ meta: {
+ label: 'Rich Text Editor [Tiptap]',
+ propertyEditorSchemaAlias: 'Umbraco.RichText',
+ icon: 'icon-browser-window',
+ group: 'richContent',
+ settings: {
+ properties: [
+ {
+ alias: 'toolbar',
+ label: 'Toolbar',
+ description: 'Pick the toolbar items that should be available when editing',
+ propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration',
+ weight: 5,
+ },
+ {
+ alias: 'extensions',
+ label: 'Extensions',
+ description: 'Extensions to enable',
+ propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration',
+ weight: 10,
+ },
+ {
+ alias: 'dimensions',
+ label: 'Dimensions',
+ description: 'Set the maximum width and height of the editor',
+ propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.DimensionsConfiguration',
+ weight: 20,
+ },
+ {
+ alias: 'maxImageSize',
+ label: 'Maximum size for inserted images',
+ description: 'Maximum width or height - enter 0 to disable resizing',
+ propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.MaxImageSizeConfiguration',
+ weight: 40,
+ },
+ {
+ alias: 'overlaySize',
+ label: 'Overlay Size',
+ description: 'Select the width of the overlay (link picker)',
+ propertyEditorUiAlias: 'Umb.PropertyEditorUi.OverlaySize',
+ weight: 50,
+ },
+ ],
+ defaultData: [
+ {
+ alias: 'toolbar',
+ value: [
+ [
+ ['Umb.Tiptap.Toolbar.SourceEditor'],
+ ['Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.Underline'],
+ [
+ 'Umb.Tiptap.Toolbar.TextAlignLeft',
+ 'Umb.Tiptap.Toolbar.TextAlignCenter',
+ 'Umb.Tiptap.Toolbar.TextAlignRight',
+ ],
+ ['Umb.Tiptap.Toolbar.BulletList', 'Umb.Tiptap.Toolbar.OrderedList'],
+ ['Umb.Tiptap.Toolbar.Blockquote', 'Umb.Tiptap.Toolbar.HorizontalRule'],
+ ['Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'],
+ ['Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.EmbeddedMedia'],
+ ],
+ ],
+ },
+ { alias: 'maxImageSize', value: 500 },
+ { alias: 'overlaySize', value: 'medium' },
+ ],
+ },
+ },
+ },
+ {
+ type: 'propertyEditorUi',
+ alias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration',
+ name: 'Tiptap Toolbar Property Editor UI',
+ js: () => import('./components/property-editor-ui-tiptap-toolbar-configuration.element.js'),
+ meta: {
+ label: 'Tiptap Toolbar Configuration',
+ icon: 'icon-autofill',
+ group: 'common',
+ },
+ },
+ {
+ type: 'propertyEditorUi',
+ alias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration',
+ name: 'Tiptap Extensions Property Editor UI',
+ js: () => import('./components/property-editor-ui-tiptap-extensions-configuration.element.js'),
+ meta: {
+ label: 'Tiptap Extensions Configuration',
+ icon: 'icon-autofill',
+ group: 'common',
+ },
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts
new file mode 100644
index 0000000000..8d56fc1943
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts
@@ -0,0 +1,60 @@
+import type { UmbInputTiptapElement } from '../../components/input-tiptap/input-tiptap.element.js';
+import { UmbRteBaseElement } from '../../../components/rte-base.element.js';
+import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
+
+import '../../components/input-tiptap/input-tiptap.element.js';
+
+const elementName = 'umb-property-editor-ui-tiptap';
+
+/**
+ * @element umb-property-editor-ui-tiptap
+ */
+@customElement(elementName)
+export class UmbPropertyEditorUiTiptapElement extends UmbRteBaseElement {
+ #onChange(event: CustomEvent & { target: UmbInputTiptapElement }) {
+ const value = event.target.value;
+
+ // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup.
+ const usedContentKeys: string[] = [];
+
+ // Regex matching all block elements in the markup, and extracting the content key. It's the same as the one used on the backend.
+ const regex = new RegExp(
+ /(?:)?<\/umb-rte-block(?:-inline)?>/gi,
+ );
+ let blockElement: RegExpExecArray | null;
+ while ((blockElement = regex.exec(value)) !== null) {
+ if (blockElement.groups?.key) {
+ usedContentKeys.push(blockElement.groups.key);
+ }
+ }
+
+ this._filterUnusedBlocks(usedContentKeys);
+
+ this._latestMarkup = value;
+
+ this._value = {
+ ...this._value,
+ markup: this._latestMarkup,
+ };
+
+ this._fireChangeEvent();
+ }
+
+ override render() {
+ return html`
+
+ `;
+ }
+}
+
+export { UmbPropertyEditorUiTiptapElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPropertyEditorUiTiptapElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.stories.ts
new file mode 100644
index 0000000000..e104bbe2e9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.stories.ts
@@ -0,0 +1,91 @@
+import type { UmbPropertyEditorUiTiptapElement } from './property-editor-ui-tiptap.element.js';
+import type { Meta, StoryObj } from '@storybook/web-components';
+import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
+
+import './property-editor-ui-tiptap.element.js';
+
+const config = new UmbPropertyEditorConfigCollection([
+ {
+ alias: 'hideLabel',
+ value: true,
+ },
+ { alias: 'dimensions', value: { height: 500 } },
+ { alias: 'maxImageSize', value: 500 },
+ { alias: 'ignoreUserStartNodes', value: false },
+ {
+ alias: 'toolbar',
+ value: [
+ [
+ [
+ ['Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.Underline'],
+ [
+ 'Umb.Tiptap.Toolbar.TextAlignLeft',
+ 'Umb.Tiptap.Toolbar.TextAlignCenter',
+ 'Umb.Tiptap.Toolbar.TextAlignRight',
+ ],
+ ['Umb.Tiptap.Toolbar.Heading1', 'Umb.Tiptap.Toolbar.Heading2', 'Umb.Tiptap.Toolbar.Heading3'],
+ ['Umb.Tiptap.Toolbar.Unlink', 'Umb.Tiptap.Toolbar.Link'],
+ ['Umb.Tiptap.Toolbar.Embed', 'Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.BlockPicker'],
+ ['Umb.Tiptap.Toolbar.Redo', 'Umb.Tiptap.Toolbar.Undo'],
+ ],
+ ],
+ ],
+ },
+ {
+ alias: 'extensions',
+ value: [
+ 'Umb.Tiptap.Bold',
+ 'Umb.Tiptap.Italic',
+ 'Umb.Tiptap.Underline',
+ 'Umb.Tiptap.Strike',
+ 'Umb.Tiptap.Blockquote',
+ 'Umb.Tiptap.CodeBlock',
+ 'Umb.Tiptap.HorizontalRule',
+ 'Umb.Tiptap.Figure',
+ 'Umb.Tiptap.Table',
+ 'Umb.Tiptap.Link',
+ 'Umb.Tiptap.Embed',
+ 'Umb.Tiptap.Image',
+ 'Umb.Tiptap.Heading',
+ 'Umb.Tiptap.List',
+ 'Umb.Tiptap.TextAlign',
+ 'Umb.Tiptap.MediaUpload',
+ 'Umb.Tiptap.Block',
+ ],
+ },
+]);
+
+const meta: Meta = {
+ title: 'Property Editor UIs/Tiptap',
+ component: 'umb-property-editor-ui-tiptap',
+ id: 'umb-property-editor-ui-tiptap',
+ args: {
+ config: undefined,
+ value: {
+ blocks: {
+ layout: {},
+ contentData: [],
+ settingsData: [],
+ expose: [],
+ },
+ markup: `
+ Tiptap
+ I am a default value for the Tiptap text editor story.
+
+ Umbraco documentation
+
+ `,
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const DefaultConfig: Story = {
+ args: {
+ config,
+ },
+};
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.test.ts
new file mode 100644
index 0000000000..214fa7d487
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.test.ts
@@ -0,0 +1,21 @@
+import { UmbPropertyEditorUiTiptapElement } from './property-editor-ui-tiptap.element.js';
+import { expect, fixture, html } from '@open-wc/testing';
+import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
+
+describe('UmbPropertyEditorUITiptapElement', () => {
+ let element: UmbPropertyEditorUiTiptapElement;
+
+ beforeEach(async () => {
+ element = await fixture(html` `);
+ });
+
+ it('is defined with its own instance', () => {
+ expect(element).to.be.instanceOf(UmbPropertyEditorUiTiptapElement);
+ });
+
+ if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
+ it('passes the a11y audit', async () => {
+ await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
+ });
+ }
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/types.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/types.ts
new file mode 100644
index 0000000000..5ce3b8d84d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/types.ts
@@ -0,0 +1,14 @@
+import type { UmbBlockValueType } from '@umbraco-cms/backoffice/block';
+import type { UmbBlockRteLayoutModel } from '@umbraco-cms/backoffice/block-rte';
+
+export interface UmbPropertyEditorUiValueType {
+ markup: string;
+ blocks: UmbBlockValueType;
+}
+
+export const UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS = 'Umbraco.RichText';
+
+/**
+ * The attribute where the block content key is stored.
+ */
+export const UMB_BLOCK_RTE_DATA_CONTENT_KEY = 'data-content-key';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/umbraco-package.ts
new file mode 100644
index 0000000000..68eece9518
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/umbraco-package.ts
@@ -0,0 +1,9 @@
+export const name = 'Umbraco.Core.Rte';
+export const extensions = [
+ {
+ name: 'RTE Bundle',
+ alias: 'Umb.Bundle.Rte',
+ type: 'bundle',
+ js: () => import('./manifests.js'),
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts
new file mode 100644
index 0000000000..20ec618303
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite';
+import { rmSync } from 'fs';
+import { getDefaultConfig } from '../../vite-config-base';
+
+const dist = '../../../dist-cms/packages/rte';
+
+// delete the unbundled dist folder
+rmSync(dist, { recursive: true, force: true });
+
+export default defineConfig({
+ ...getDefaultConfig({
+ dist,
+ entry: {
+ 'tiny-mce/index': 'tiny-mce/index.ts',
+ 'tiptap/index': 'tiptap/index.ts',
+ manifests: 'manifests.ts',
+ 'umbraco-package': 'umbraco-package.ts',
+ },
+ }),
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts
deleted file mode 100644
index 011799ec00..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './media-caption-alt-text/index.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/types.ts
deleted file mode 100644
index 35c975e701..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { UmbBlockValueDataPropertiesBaseType } from '@umbraco-cms/backoffice/block';
-
-export interface UmbPropertyEditorUiValueType {
- markup: string;
- blocks: UmbBlockValueDataPropertiesBaseType;
-}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts
deleted file mode 100644
index a5de5f04cd..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const name = 'Umbraco.Core.TinyMce';
-export const extensions = [
- {
- name: 'TinyMce Bundle',
- alias: 'Umb.Bundle.TinyMce',
- type: 'bundle',
- js: () => import('./manifests.js'),
- },
-];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts
deleted file mode 100644
index d0c457d1e9..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from 'vite';
-import { rmSync } from 'fs';
-import { getDefaultConfig } from '../../vite-config-base';
-
-const dist = '../../../dist-cms/packages/tiny-mce';
-
-// delete the unbundled dist folder
-rmSync(dist, { recursive: true, force: true });
-
-export default defineConfig({
- ...getDefaultConfig({ dist }),
-});
diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json
index 2f92b70db4..08f7cd7734 100644
--- a/src/Umbraco.Web.UI.Client/tsconfig.json
+++ b/src/Umbraco.Web.UI.Client/tsconfig.json
@@ -121,7 +121,8 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/template": ["./src/packages/templating/templates/index.ts"],
"@umbraco-cms/backoffice/temporary-file": ["./src/packages/core/temporary-file/index.ts"],
"@umbraco-cms/backoffice/themes": ["./src/packages/core/themes/index.ts"],
- "@umbraco-cms/backoffice/tiny-mce": ["./src/packages/tiny-mce/index.ts"],
+ "@umbraco-cms/backoffice/tiny-mce": ["./src/packages/rte/tiny-mce/index.ts"],
+ "@umbraco-cms/backoffice/tiptap": ["./src/packages/rte/tiptap/index.ts"],
"@umbraco-cms/backoffice/translation": ["./src/packages/translation/index.ts"],
"@umbraco-cms/backoffice/tree": ["./src/packages/core/tree/index.ts"],
"@umbraco-cms/backoffice/ufm": ["./src/packages/ufm/index.ts"],
@@ -145,6 +146,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/external/router-slot": ["./src/external/router-slot/index.ts"],
"@umbraco-cms/backoffice/external/rxjs": ["./src/external/rxjs/index.ts"],
"@umbraco-cms/backoffice/external/tinymce": ["./src/external/tinymce/index.ts"],
+ "@umbraco-cms/backoffice/external/tiptap": ["./src/external/tiptap/index.ts"],
"@umbraco-cms/backoffice/external/uui": ["./src/external/uui/index.ts"],
"@umbraco-cms/backoffice/external/uuid": ["./src/external/uuid/index.ts"]
}