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}">`); + }); + } } 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`);