diff --git a/src/Umbraco.Web.UI.Client/index.html b/src/Umbraco.Web.UI.Client/index.html
index b92c54f0d6..5b7473a4d2 100644
--- a/src/Umbraco.Web.UI.Client/index.html
+++ b/src/Umbraco.Web.UI.Client/index.html
@@ -6,6 +6,13 @@
Umbraco
+
diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts
index 4fef647c2e..0145765978 100644
--- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts
+++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts
@@ -1792,13 +1792,7 @@ This is to test the default configuration of the TinyMCE editor.
Search for **dt-richTextEditorTinyMce** in the codebase to find the configuration and add configuration values.
-**NB!** If this throws an error in console, go to \`input-tiny-mce.defaults.ts\` and comment out the script append on line 126:
-
-\`\`\`js
-script.text = \`import "@umbraco-cms/backoffice/extension-registry";\`;
-script.text = \`import "\${UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH}";\`;
-//editor.dom.doc.head.appendChild(script);
-\`\`\``,
+**NB!** If this throws an error in console, make sure that \`@umbraco-cms/backoffice/block-rte\` is available in the importmap.`,
dataType: { id: 'dt-richTextEditorTinyMce' },
variesByCulture: false,
variesBySegment: false,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts
index d41818eae6..36f849fa13 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts
@@ -14,7 +14,7 @@ export interface ManifestBlockEditorCustomView extends ManifestElement } forBlockEditor - Declare if this Custom View only must appear at specific Block Editors.
* @description Optional condition if you like this custom view to only appear at a specific type of Block Editor.
* @example 'block-list'
- * @example ['block-list', 'block-grid']
+ * @example ['block-list', 'block-grid', 'block-rte']
*/
forBlockEditor?: string | Array;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts
index 416ce658ba..d85cf71cea 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts
@@ -171,7 +171,7 @@ export class UmbBlockGridManagerContext<
originData: UmbBlockGridWorkspaceOriginData,
) {
this.setOneLayout(layoutEntry, originData);
- this.insertBlockData(layoutEntry, content, settings, originData);
+ this.insertBlockData(layoutEntry, content, settings);
return true;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts
index 1acd32840f..86d95a3cd5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts
@@ -39,7 +39,7 @@ export class UmbBlockListManagerContext<
) {
this._layouts.appendOneAt(layoutEntry, originData.index ?? -1);
- this.insertBlockData(layoutEntry, content, settings, originData);
+ this.insertBlockData(layoutEntry, content, settings);
return true;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts
index 54a59940f8..1e6d55a034 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts
@@ -1,10 +1,14 @@
-import type { UmbBlockRteLayoutModel } from '../../types.js';
+import { UMB_BLOCK_RTE, type UmbBlockRteLayoutModel } from '../../types.js';
import { UmbBlockRteEntryContext } from '../../context/block-rte-entry.context.js';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, css, property, state, customElement } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import type { UmbBlockEditorCustomViewProperties } from '@umbraco-cms/backoffice/block-custom-view';
+import type {
+ ManifestBlockEditorCustomView,
+ UmbBlockEditorCustomViewProperties,
+} from '@umbraco-cms/backoffice/block-custom-view';
+import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import '../ref-rte-block/index.js';
@@ -13,22 +17,22 @@ import '../ref-rte-block/index.js';
*/
@customElement('umb-rte-block')
export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement {
- //
@property({ type: String, attribute: 'data-content-udi', reflect: true })
public get contentUdi(): string | undefined {
- return this._contentUdi;
+ return this.#contentUdi;
}
public set contentUdi(value: string | undefined) {
if (!value) return;
- this._contentUdi = value;
+ this.#contentUdi = value;
this.#context.setContentUdi(value);
}
- private _contentUdi?: string | undefined;
+ #contentUdi?: string;
#context = new UmbBlockRteEntryContext(this);
@state()
_showContentEdit = false;
+
@state()
_hasSettings = false;
@@ -44,6 +48,9 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
@state()
_workspaceEditSettingsPath?: string;
+ @state()
+ _contentElementTypeAlias?: string;
+
@state()
_blockViewProps: UmbBlockEditorCustomViewProperties = {
contentUdi: undefined!,
@@ -69,6 +76,9 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
this._hasSettings = !!key;
this.#updateBlockViewProps({ config: { ...this._blockViewProps.config, showSettingsEdit: !!key } });
});
+ this.observe(this.#context.contentElementTypeAlias, (alias) => {
+ this._contentElementTypeAlias = alias;
+ });
this.observe(
this.#context.blockType,
(blockType) => {
@@ -142,16 +152,26 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
return html``;
}
+ #filterBlockCustomViews = (manifest: ManifestBlockEditorCustomView) => {
+ const elementTypeAlias = this._contentElementTypeAlias ?? '';
+ const isForBlockEditor =
+ !manifest.forBlockEditor || stringOrStringArrayContains(manifest.forBlockEditor, UMB_BLOCK_RTE);
+ const isForContentTypeAlias =
+ !manifest.forContentTypeAlias || stringOrStringArrayContains(manifest.forContentTypeAlias, elementTypeAlias);
+ return isForBlockEditor && isForContentTypeAlias;
+ };
+
#renderBlock() {
return html`
${this.#renderRefBlock()}
+ .filter=${this.#filterBlockCustomViews}
+ single>
+ ${this.#renderRefBlock()}
+
${this._showContentEdit && this._workspaceEditContentPath
? html`
@@ -163,9 +183,6 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
`
: ''}
- this.#context.requestDelete()}>
-
-
`;
@@ -175,7 +192,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
return this.#renderBlock();
}
- static override styles = [
+ static override readonly styles = [
UmbTextStyles,
css`
:host {
@@ -183,6 +200,13 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
display: block;
user-select: none;
user-drag: auto;
+ white-space: nowrap;
+ }
+ :host(.ProseMirror-selectednode) {
+ umb-ref-rte-block {
+ cursor: not-allowed;
+ outline: 3px solid #b4d7ff;
+ }
}
uui-action-bar {
position: absolute;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts
index 153538eaa9..c20abdfb1b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts
@@ -32,13 +32,16 @@ export class UmbRefRteBlockElement extends UmbLitElement {
}
override render() {
- return html``;
+ return html`
+
+ `;
}
- static override styles = [
+ static override readonly styles = [
css`
+ :host {
+ display: block;
+ }
uui-ref-node {
min-height: var(--uui-size-16);
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts
index ac2501c80a..7b356d3eea 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts
@@ -50,15 +50,9 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
value.create.contentElementTypeKey,
// We can parse an empty object, cause the rest will be filled in by others.
{} as any,
- data.originData as UmbBlockRteWorkspaceOriginData,
);
if (created) {
- this.insert(
- created.layout,
- created.content,
- created.settings,
- data.originData as UmbBlockRteWorkspaceOriginData,
- );
+ this.insert(created.layout, created.content, created.settings);
} else {
throw new Error('Failed to create block');
}
@@ -131,25 +125,16 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
this._manager?.setLayouts(layouts);
}
- async create(
- contentElementTypeKey: string,
- partialLayoutEntry?: Omit,
- originData?: UmbBlockRteWorkspaceOriginData,
- ) {
+ async create(contentElementTypeKey: string, partialLayoutEntry?: Omit) {
await this._retrieveManager;
- return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData);
+ return this._manager?.create(contentElementTypeKey, partialLayoutEntry);
}
// insert Block?
- async insert(
- layoutEntry: UmbBlockRteLayoutModel,
- content: UmbBlockDataType,
- settings: UmbBlockDataType | undefined,
- originData: UmbBlockRteWorkspaceOriginData,
- ) {
+ async insert(layoutEntry: UmbBlockRteLayoutModel, content: UmbBlockDataType, settings: UmbBlockDataType | undefined) {
await this._retrieveManager;
- return this._manager?.insert(layoutEntry, content, settings, originData) ?? false;
+ return this._manager?.insert(layoutEntry, content, settings) ?? false;
}
// create Block?
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts
index 8f7e86d2a9..f568f2b474 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts
@@ -1,7 +1,5 @@
import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js';
-import type { UmbBlockRteWorkspaceOriginData } from '../index.js';
import type { UmbBlockDataType } from '../../block/types.js';
-import type { Editor } from '@umbraco-cms/backoffice/external/tinymce';
import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block';
import '../components/block-rte-entry/index.js';
@@ -12,32 +10,11 @@ import '../components/block-rte-entry/index.js';
export class UmbBlockRteManagerContext<
BlockLayoutType extends UmbBlockRteLayoutModel = UmbBlockRteLayoutModel,
> extends UmbBlockManagerContext {
- //
- #editor?: Editor;
-
- setTinyMceEditor(editor: Editor) {
- this.#editor = editor;
- }
-
- getTinyMceEditor() {
- return this.#editor;
- }
-
removeOneLayout(contentUdi: string) {
this._layouts.removeOne(contentUdi);
}
- getLayouts(): Array {
- return this._layouts.getValue();
- }
-
- create(
- contentElementTypeKey: string,
- partialLayoutEntry?: Omit,
- // This property is used by some implementations, but not used in this.
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- originData?: UmbBlockRteWorkspaceOriginData,
- ) {
+ create(contentElementTypeKey: string, partialLayoutEntry?: Omit) {
const data = super.createBlockData(contentElementTypeKey, partialLayoutEntry);
// Find block type.
@@ -53,29 +30,10 @@ export class UmbBlockRteManagerContext<
return data;
}
- insert(
- layoutEntry: BlockLayoutType,
- content: UmbBlockDataType,
- settings: UmbBlockDataType | undefined,
- originData: UmbBlockRteWorkspaceOriginData,
- ) {
- if (!this.#editor) return false;
-
+ insert(layoutEntry: BlockLayoutType, content: UmbBlockDataType, settings: UmbBlockDataType | undefined) {
this._layouts.appendOne(layoutEntry);
- this.insertBlockData(layoutEntry, content, settings, originData);
-
- if (layoutEntry.displayInline) {
- this.#editor.selection.setContent(
- ``,
- );
- } else {
- this.#editor.selection.setContent(
- ``,
- );
- }
-
- this.#editor.fire('change');
+ this.insertBlockData(layoutEntry, content, settings);
return true;
}
@@ -85,13 +43,6 @@ export class UmbBlockRteManagerContext<
* @internal
*/
public deleteLayoutElement(contentUdi: string) {
- if (!this.#editor) return;
-
- const blockElementsOfThisUdi = this.#editor.dom.select(
- `umb-rte-block[data-content-udi='${contentUdi}'], umb-rte-block-inline[data-content-udi='${contentUdi}']`,
- );
- blockElementsOfThisUdi.forEach((blockElement) => {
- this.#editor?.dom.remove(blockElement);
- });
+ this.removeBlockUdi(contentUdi);
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/manifests.ts
index 0f70e08783..7b3a871ae7 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/manifests.ts
@@ -1,4 +1,9 @@
import { manifests as tinyMcePluginManifests } from './tiny-mce-plugin/manifests.js';
+import { manifests as tiptapExtensionManifests } from './tiptap-extension/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
-export const manifests: Array = [...tinyMcePluginManifests, ...workspaceManifests];
+export const manifests: Array = [
+ ...tinyMcePluginManifests,
+ ...tiptapExtensionManifests,
+ ...workspaceManifests,
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts
index c941292048..4abbf3c32a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts
@@ -1,18 +1,23 @@
+import type { UmbBlockDataType } from '../../block/types.js';
import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from '../context/block-rte-manager.context-token.js';
import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '../context/block-rte-entries.context-token.js';
+import { UMB_DATA_CONTENT_UDI, type UmbBlockRteLayoutModel } 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';
export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase {
#localize = new UmbLocalizationController(this._host);
-
- private _blocks?: Array;
+ #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 +32,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 +75,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 +88,28 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase
window.history.pushState({}, '', createPath);
}
}
+
+ #updateBlocks(blocks: UmbBlockDataType[], 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_DATA_CONTENT_UDI));
+ const newBlocks = blocks.filter((x) => !existingBlocks.find((contentUdi) => contentUdi === x.udi));
+
+ newBlocks.forEach((block) => {
+ // Find layout for block
+ const layout = layouts.find((x) => x.contentUdi === block.udi);
+ const inline = layout?.displayInline ?? false;
+
+ let blockTag = 'umb-rte-block';
+
+ if (inline) {
+ blockTag = 'umb-rte-block-inline';
+ }
+
+ editor.insertContent(`<${blockTag} data-content-udi="${block.udi}">${blockTag}>`);
+ });
+ }
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiptap-extension/block-picker.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiptap-extension/block-picker.extension.ts
new file mode 100644
index 0000000000..d70c9e9adc
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiptap-extension/block-picker.extension.ts
@@ -0,0 +1,176 @@
+import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from '../context/block-rte-manager.context-token.js';
+import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '../context/block-rte-entries.context-token.js';
+import type { UmbBlockDataType } from '../../block/types.js';
+import { UMB_DATA_CONTENT_UDI, type UmbBlockRteLayoutModel } from '../types.js';
+import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
+import { UmbTiptapToolbarElementApiBase } from '@umbraco-cms/backoffice/tiptap';
+import { Node, type Editor } from '@umbraco-cms/backoffice/external/tiptap';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import { distinctUntilChanged } from '@umbraco-cms/backoffice/external/rxjs';
+
+declare module '@tiptap/core' {
+ interface Commands {
+ umbRteBlock: {
+ setBlock: (options: { contentUdi: string }) => ReturnType;
+ };
+ umbRteBlockInline: {
+ setBlockInline: (options: { contentUdi: 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_DATA_CONTENT_UDI]: {
+ isRequired: true,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'umb-rte-block' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['umb-rte-block', HTMLAttributes];
+ },
+
+ addCommands() {
+ return {
+ setBlock:
+ (options) =>
+ ({ commands }) => {
+ const attrs = { [UMB_DATA_CONTENT_UDI]: options.contentUdi };
+ 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_DATA_CONTENT_UDI]: options.contentUdi };
+ return commands.insertContent({
+ type: this.name,
+ attrs,
+ });
+ },
+ };
+ },
+});
+
+export default class UmbTiptapBlockPickerExtension 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.observe(
+ context.contents.pipe(
+ distinctUntilChanged((prev, curr) => prev.map((y) => y.udi).join() === curr.map((y) => y.udi).join()),
+ ),
+ (contents) => {
+ this.#updateBlocks(contents, context.getLayouts());
+ },
+ 'contents',
+ );
+ });
+ this.consumeContext(UMB_BLOCK_RTE_ENTRIES_CONTEXT, (context) => {
+ this.#entriesContext = context;
+ });
+ }
+
+ getTiptapExtensions() {
+ return [umbRteBlock, umbRteBlockInline];
+ }
+
+ override isActive(editor: Editor) {
+ return (
+ editor.isActive(`umb-rte-block[${UMB_DATA_CONTENT_UDI}]`) ||
+ editor.isActive(`umb-rte-block-inline[${UMB_DATA_CONTENT_UDI}]`)
+ );
+ }
+
+ 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);
+ }
+ }
+
+ #updateBlocks(blocks: UmbBlockDataType[], 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_DATA_CONTENT_UDI),
+ );
+ const newBlocks = blocks.filter((x) => !existingBlocks.find((contentUdi) => contentUdi === x.udi));
+
+ newBlocks.forEach((block) => {
+ // Find layout for block
+ const layout = layouts.find((x) => x.contentUdi === block.udi);
+ const inline = layout?.displayInline ?? false;
+
+ if (inline) {
+ editor.commands.setBlockInline({ contentUdi: block.udi });
+ } else {
+ editor.commands.setBlock({ contentUdi: block.udi });
+ }
+ });
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiptap-extension/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiptap-extension/manifests.ts
new file mode 100644
index 0000000000..2181248d85
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/tiptap-extension/manifests.ts
@@ -0,0 +1,16 @@
+import type { ManifestTiptapExtensionButtonKind } from '@umbraco-cms/backoffice/tiptap';
+
+export const manifests: ManifestTiptapExtensionButtonKind[] = [
+ {
+ type: 'tiptapExtension',
+ kind: 'button',
+ alias: 'Umb.TiptapExtension.BlockPicker',
+ name: 'Block Picker Tiptap Extension Button',
+ api: () => import('./block-picker.extension.js'),
+ meta: {
+ alias: 'umbblockpicker',
+ icon: 'icon-plugin',
+ label: '#blockEditor_insertBlock',
+ },
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts
index c97075ec68..dca59515c5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts
@@ -2,6 +2,8 @@ import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
import type { UmbBlockLayoutBaseModel, UmbBlockValueType } from '@umbraco-cms/backoffice/block';
export const UMB_BLOCK_RTE_TYPE = 'block-rte-type';
+export const UMB_BLOCK_RTE = 'block-rte';
+export const UMB_DATA_CONTENT_UDI = 'data-content-udi';
export interface UmbBlockRteTypeModel extends UmbBlockTypeBaseModel {
displayInline: boolean;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts
index b56ef6ac6f..ce98e0bee7 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts
@@ -86,6 +86,9 @@ export abstract class UmbBlockManagerContext<
setLayouts(layouts: Array) {
this._layouts.setValue(layouts);
}
+ getLayouts() {
+ return this._layouts.getValue();
+ }
setContents(contents: Array) {
this.#contents.setValue(contents);
}
@@ -276,16 +279,12 @@ export abstract class UmbBlockManagerContext<
layoutEntry: BlockLayoutType,
content: UmbBlockDataType,
settings: UmbBlockDataType | undefined,
- // TODO: [v15]: ignoring unused var here here to prevent a breaking change
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- originData: BlockOriginDataType,
) {
// Create content entry:
if (layoutEntry.contentUdi) {
this.#contents.appendOne(content);
} else {
throw new Error('Cannot create block, missing contentUdi');
- return false;
}
//Create settings entry:
@@ -293,4 +292,8 @@ export abstract class UmbBlockManagerContext<
this.#settings.appendOne(settings);
}
}
+
+ protected removeBlockUdi(contentUdi: string) {
+ this.#contents.removeOne(contentUdi);
+ }
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/custom-view/manifest.ts b/src/Umbraco.Web.UI.Client/src/packages/block/custom-view/manifest.ts
index 46e74f3b3a..317189167a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/custom-view/manifest.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/custom-view/manifest.ts
@@ -1,4 +1,6 @@
-export const manifest: UmbExtensionManifest = {
+import type { ManifestBlockEditorCustomView } from '../block-custom-view/block-editor-custom-view.extension.js';
+
+export const manifest: ManifestBlockEditorCustomView = {
type: 'blockEditorCustomView',
alias: 'Umb.blockEditorCustomView.TestView',
name: 'Block Editor Custom View Test',
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
index 0aa053cde2..b37d8d3f0c 100644
--- 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
@@ -95,6 +95,9 @@ export class UmbInputTiptapElement extends UmbFormControlMixin {
+ this._extensions.forEach((ext) => ext.setEditor(editor));
+ },
onUpdate: ({ editor }) => {
this.#markup = editor.getHTML();
this.dispatchEvent(new UmbChangeEvent());
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
index fc3c7219bd..d412ec4682 100644
--- 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
@@ -45,8 +45,8 @@ export class UmbTiptapToolbarButtonElement extends UmbLitElement {
compact
look=${this._isActive ? 'outline' : 'default'}
label=${ifDefined(this.manifest?.meta.label)}
- title=${this.manifest?.meta.label ? this.localize.term(this.manifest.meta.label) : ''}
- @click=${() => this.api?.execute(this.editor)}>
+ title=${this.manifest?.meta.label ? this.localize.string(this.manifest.meta.label) : ''}
+ @click=${() => (this.api && this.editor ? this.api.execute(this.editor) : null)}>
${when(
this.manifest?.meta.icon,
() => html``,
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
index bf8b9956e9..7254da3b61 100644
--- 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
@@ -1,7 +1,7 @@
-import { UmbTiptapToolbarElementApiBase } from '../types.js';
+import { UmbTiptapExtensionApiBase } from '../types.js';
import { UmbImage } from '@umbraco-cms/backoffice/external/tiptap';
-export default class UmbTiptapImageExtensionApi extends UmbTiptapToolbarElementApiBase {
+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/types.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts
index b10b98f2a3..2ab9a0179e 100644
--- 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
@@ -5,12 +5,38 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
export interface UmbTiptapExtensionApi extends UmbApi {
+ /**
+ * 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 {
- public manifest?: ManifestTiptapExtension;
+ /**
+ * The manifest for the extension.
+ */
+ protected _manifest?: ManifestTiptapExtension;
+ /**
+ * The editor instance.
+ */
+ protected _editor?: Editor;
+
+ /**
+ * @inheritdoc
+ */
+ setEditor(editor: Editor): void {
+ this._editor = editor;
+ }
+
+ /**
+ * @inheritdoc
+ */
abstract getTiptapExtensions(args?: UmbTiptapExtensionArgs): Array;
}
@@ -24,18 +50,31 @@ export interface UmbTiptapExtensionArgs {
}
export interface UmbTiptapToolbarElementApi extends UmbTiptapExtensionApi {
- execute(editor?: Editor): void;
- isActive(editor?: Editor): boolean;
+ /**
+ * 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 UmbTiptapExtensionApiBase
implements UmbTiptapToolbarElementApi
{
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- public execute(editor?: Editor) {}
+ /**
+ * 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 {ManifestTiptapExtension}
+ */
public isActive(editor?: Editor) {
- return editor && this.manifest?.meta.alias ? editor?.isActive(this.manifest.meta.alias) : false;
+ return editor && this._manifest?.meta.alias ? editor?.isActive(this._manifest.meta.alias) : false;
}
}
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
index a4fac1bf92..fcd9ce238a 100644
--- 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
@@ -10,13 +10,12 @@ import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extensi
import '../../components/input-tiptap/input-tiptap.element.js';
import type { UmbBlockValueType } from '@umbraco-cms/backoffice/block';
-// Look at Tiny for correct types
export interface UmbRichTextEditorValueType {
markup: string;
blocks: UmbBlockValueType;
}
-const UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS = 'Umbraco.RichText';
+const UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS = 'Umbraco.TinyMCE';
const elementName = 'umb-property-editor-ui-tiptap';
@@ -128,21 +127,24 @@ export class UmbPropertyEditorUiTiptapElement extends UmbLitElement implements U
markup: this._latestMarkup,
};
- // TODO: Validate blocks
- // 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');
- });
-
// 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 usedContentUdis = Array.from(blockEls).map((blockElement) => blockElement.getAttribute('data-content-udi'));
+ const usedContentUdis: string[] = [];
+
+ // Regex matching all block elements in the markup, and extracting the content UDI. 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(this._latestMarkup)) !== null) {
+ if (blockElement.groups?.udi) {
+ usedContentUdis.push(blockElement.groups.udi);
+ }
+ }
+
const unusedBlocks = this.#managerContext.getLayouts().filter((x) => usedContentUdis.indexOf(x.contentUdi) === -1);
unusedBlocks.forEach((blockLayout) => {
this.#managerContext.removeOneLayout(blockLayout.contentUdi);
- });*/
+ });
this.#fireChangeEvent();
}
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
index 1385afcf23..231d180c29 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts
@@ -8,5 +8,12 @@ const dist = '../../../dist-cms/packages/rte';
rmSync(dist, { recursive: true, force: true });
export default defineConfig({
- ...getDefaultConfig({ dist }),
+ ...getDefaultConfig({
+ dist,
+ entry: {
+ '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/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
index 8e8f81cafa..e2d3c3b877 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/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts
@@ -380,7 +380,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/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts
index 8cb80938c6..7f95d97cdf 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/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts
@@ -1,3 +1,4 @@
+import type { UmbInputTinyMceElement } from '../../components/input-tiny-mce/input-tiny-mce.element.js';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
@@ -115,13 +116,12 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
- #onChange() {
- const editor = this.#managerContext.getTinyMceEditor();
- if (!editor) return;
+ #onChange(event: CustomEvent & { target: UmbInputTinyMceElement }) {
+ const value = event.target.value;
// Clone the DOM, to remove the classes and attributes on the original:
const div = document.createElement('div');
- div.innerHTML = editor.getContent();
+ div.innerHTML = value.toString();
// Loop through used, to remove the classes on these.
const blockEls = div.querySelectorAll(`umb-rte-block, umb-rte-block-inline`);