From 8935b68705ed8ee9762b2fd31fce89c51e848449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sun, 26 May 2024 10:48:27 +0200 Subject: [PATCH] implement manager for rte property editor --- .../block/block-list/context/index.ts | 1 + .../block-rte-entries.context-token.ts | 5 + .../context/block-rte-entries.context.ts | 121 ++++++++++++++++++ .../context/block-rte-entry.context-token.ts | 5 + .../context/block-rte-entry.context.ts | 26 ++++ .../context/block-rte-manager.context.ts | 47 +++++++ .../packages/block/block-rte/context/index.ts | 5 + .../src/packages/block/block-rte/index.ts | 1 + .../tiny-mce-block-picker.plugin.ts | 14 +- .../src/packages/block/block-rte/types.ts | 4 + .../block-rte-workspace.modal-token.ts | 8 +- .../src/packages/block/block/types.ts | 7 +- .../property-editor-ui-tiny-mce.element.ts | 64 +++++++-- 13 files changed, 287 insertions(+), 21 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/index.ts index 235016411e..0471c6a1a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/index.ts @@ -1 +1,2 @@ +export * from './block-list-entries.context-token.js'; export * from './block-list-entry.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts new file mode 100644 index 0000000000..bda7bbbaa5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts @@ -0,0 +1,5 @@ +import type { UmbBlockRteEntriesContext } from './block-rte-entries.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_RTE_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); 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 new file mode 100644 index 0000000000..246abf9f49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts @@ -0,0 +1,121 @@ +import type { UmbBlockDataType } from '../../block/index.js'; +import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '../../block/index.js'; +import type { UmbBlockRteWorkspaceData } from '../index.js'; +import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; +import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from './block-rte-manager.context.js'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; + +export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT, + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT.TYPE, + UmbBlockRteTypeModel, + UmbBlockRteLayoutModel +> { + // + #catalogueModal: UmbModalRouteRegistrationController; + + // We will just say its always allowed for RTE for now: [NL] + public readonly canCreate = new UmbBooleanState(true).asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_BLOCK_RTE_MANAGER_CONTEXT); + + this.#catalogueModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL) + .addUniquePaths(['propertyAlias', 'variantId']) + .addAdditionalPath(':view/:index') + .onSetup((routingInfo) => { + // Idea: Maybe on setup should be async, so it can retrieve the values when needed? [NL] + const index = routingInfo.index ? parseInt(routingInfo.index) : -1; + return { + data: { + blocks: this._manager?.getBlockTypes() ?? [], + blockGroups: [], + openClipboard: routingInfo.view === 'clipboard', + blockOriginData: { index: index }, + }, + }; + }) + .observeRouteBuilder((routeBuilder) => { + this._catalogueRouteBuilderState.setValue(routeBuilder); + }); + } + + protected _gotBlockManager() { + if (!this._manager) return; + + this.observe( + this._manager.layouts, + (layouts) => { + this._layoutEntries.setValue(layouts); + }, + 'observeParentLayouts', + ); + this.observe( + this.layoutEntries, + (layouts) => { + this._manager?.setLayouts(layouts); + }, + 'observeThisLayouts', + ); + + this.observe( + this._manager.propertyAlias, + (alias) => { + this.#catalogueModal.setUniquePathValue('propertyAlias', alias ?? 'null'); + }, + 'observePropertyAlias', + ); + + this.observe( + this._manager.variantId, + (variantId) => { + if (variantId) { + this.#catalogueModal.setUniquePathValue('variantId', variantId.toString()); + } + }, + 'observePropertyAlias', + ); + } + + getPathForCreateBlock(index: number) { + return this._catalogueRouteBuilderState.getValue()?.({ view: 'create', index: index }); + } + + getPathForClipboard(index: number) { + return this._catalogueRouteBuilderState.getValue()?.({ view: 'clipboard', index: index }); + } + + async setLayouts(layouts: Array) { + await this._retrieveManager; + this._manager?.setLayouts(layouts); + } + + async create( + contentElementTypeKey: string, + partialLayoutEntry?: Omit, + modalData?: UmbBlockRteWorkspaceData, + ) { + await this._retrieveManager; + return this._manager?.create(contentElementTypeKey, partialLayoutEntry, modalData); + } + + // insert Block? + + async insert( + layoutEntry: UmbBlockRteLayoutModel, + content: UmbBlockDataType, + settings: UmbBlockDataType | undefined, + modalData: UmbBlockRteWorkspaceData, + ) { + await this._retrieveManager; + return this._manager?.insert(layoutEntry, content, settings, modalData) ?? false; + } + + // create Block? + async delete(contentUdi: string) { + // TODO: Loop through children and delete them as well? + await super.delete(contentUdi); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context-token.ts new file mode 100644 index 0000000000..9bf50d97a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context-token.ts @@ -0,0 +1,5 @@ +import type { UmbBlockRteEntryContext } from './block-rte-entry.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_RTE_ENTRY_CONTEXT = new UmbContextToken('UmbBlockEntryContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context.ts new file mode 100644 index 0000000000..87011540af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entry.context.ts @@ -0,0 +1,26 @@ +import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from './block-rte-manager.context.js'; +import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from './block-rte-entries.context-token.js'; +import { UmbBlockEntryContext } from '@umbraco-cms/backoffice/block'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +export class UmbBlockRteEntryContext extends UmbBlockEntryContext< + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT, + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT.TYPE, + typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT, + typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE +> { + readonly forceHideContentEditorInOverlay = this._blockType.asObservablePart( + (x) => !!x?.forceHideContentEditorInOverlay, + ); + + readonly showContentEdit = this.forceHideContentEditorInOverlay; + + constructor(host: UmbControllerHost) { + super(host, UMB_BLOCK_RTE_MANAGER_CONTEXT, UMB_BLOCK_RTE_ENTRIES_CONTEXT); + } + + _gotManager() {} + + _gotEntries() {} + + _gotContentType() {} +} 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 new file mode 100644 index 0000000000..da8c7b404a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts @@ -0,0 +1,47 @@ +import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; +import type { UmbBlockRteWorkspaceData } from '../index.js'; +import type { UmbBlockDataType } from '../../block/types.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; + +/** + * A implementation of the Block Manager specifically for the Rich Text Editor. + */ +export class UmbBlockRteManagerContext< + BlockLayoutType extends UmbBlockRteLayoutModel = UmbBlockRteLayoutModel, +> extends UmbBlockManagerContext { + // + #inlineEditingMode = new UmbBooleanState(undefined); + readonly inlineEditingMode = this.#inlineEditingMode.asObservable(); + + setInlineEditingMode(inlineEditingMode: boolean | undefined) { + this.#inlineEditingMode.setValue(inlineEditingMode ?? false); + } + + create( + contentElementTypeKey: string, + partialLayoutEntry?: Omit, + modalData?: UmbBlockRteWorkspaceData, + ) { + return super.createBlockData(contentElementTypeKey, partialLayoutEntry); + } + + insert( + layoutEntry: BlockLayoutType, + content: UmbBlockDataType, + settings: UmbBlockDataType | undefined, + modalData: UmbBlockRteWorkspaceData, + ) { + this._layouts.appendOneAt(layoutEntry, modalData.originData.index ?? -1); + + this.insertBlockData(layoutEntry, content, settings, modalData); + + return true; + } +} + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_RTE_MANAGER_CONTEXT = new UmbContextToken( + 'UmbBlockManagerContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/index.ts new file mode 100644 index 0000000000..18dc42722d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/index.ts @@ -0,0 +1,5 @@ +export * from './block-rte-entries.context-token.js'; +export * from './block-rte-entries.context.js'; +export * from './block-rte-entry.context-token.js'; +export * from './block-rte-entry.context.js'; +export * from './block-rte-manager.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/index.ts index 56ebdc81cc..4cf976fea5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/index.ts @@ -1,2 +1,3 @@ +export * from './context/index.js'; export * from './workspace/index.js'; export * from './types.js'; 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 f8f11ce8a4..40ebd0d7ef 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 @@ -40,16 +40,20 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase async showDialog() { const blockEl = this.editor.selection.getNode(); - let blockUdi: string | undefined; if (blockEl.nodeName === 'UMB-RTE-BLOCK' || blockEl.nodeName === 'UMB-RTE-BLOCK-INLINE') { - blockUdi = blockEl.getAttribute('data-content-udi') ?? undefined; + const blockUdi = blockEl.getAttribute('data-content-udi') ?? undefined; + if (blockUdi) { + this.#editBlock(blockUdi); + return; + } } - this.#openBlockPicker(blockUdi); + // If no block is selected, open the block picker: + this.#createBlock(); } - async #openBlockPicker(blockUdi?: string) { + async #editBlock(blockUdi?: string) { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const modalHandler = modalManager.open(this, UMB_BLOCK_RTE_WORKSPACE_MODAL, { data: { @@ -69,6 +73,8 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase } } + #createBlock() {} + #insertBlockInEditor(blockContentUdi: string, displayInline = false) { if (!blockContentUdi) { return; 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 c88c4a5879..5720048a89 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 @@ -1,5 +1,9 @@ import type { UmbBlockTypeBaseModel } from '../block-type/index.js'; +import type { UmbBlockLayoutBaseModel, UmbBlockValueType } from '@umbraco-cms/backoffice/block'; export const UMB_BLOCK_RTE_TYPE = 'block-rte-type'; export interface UmbBlockRteTypeModel extends UmbBlockTypeBaseModel {} +export interface UmbBlockRteLayoutModel extends UmbBlockLayoutBaseModel {} + +export interface UmbBlockRteValueModel extends UmbBlockValueType {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts index 6db8219685..edd5057d9e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts @@ -6,14 +6,14 @@ import type { import type { UmbWorkspaceModalData } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export interface UmbBlockRTEWorkspaceData +export interface UmbBlockRteWorkspaceData extends UmbBlockWorkspaceData<{ index: number; }> {} -export type UmbBlockRTEWorkspaceValue = Array>; +export type UmbBlockRteWorkspaceValue = Array>; -export const UMB_BLOCK_RTE_WORKSPACE_MODAL = new UmbModalToken( +export const UMB_BLOCK_RTE_WORKSPACE_MODAL = new UmbModalToken( 'Umb.Modal.Workspace', { modal: { @@ -23,4 +23,4 @@ export const UMB_BLOCK_RTE_WORKSPACE_MODAL = new UmbModalToken, UmbBlockRTEWorkspaceValue>; +) as UmbModalToken, UmbBlockRteWorkspaceValue>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/types.ts index 7f1289a083..f89b649301 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/types.ts @@ -9,12 +9,15 @@ export interface UmbBlockDataType { [key: string]: unknown; } -export interface UmbBlockValueType { - layout: { [key: string]: Array | undefined }; +export interface UmbBlockDataBaseValueType { contentData: Array; settingsData: Array; } +export interface UmbBlockValueType extends UmbBlockDataBaseValueType { + layout: { [key: string]: Array | undefined }; +} + export interface UmbBlockViewUrlsPropType { editContent?: string; editSettings?: string; 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 9731af8f9d..703b2e841b 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,15 +1,16 @@ -import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +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'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import '../../components/input-tiny-mce/input-tiny-mce.element.js'; +import { UmbBlockRteEntriesContext, UmbBlockRteManagerContext } from '@umbraco-cms/backoffice/block-rte'; +import type { UmbBlockDataBaseValueType } from '@umbraco-cms/backoffice/block'; -type RichTextEditorValue = { - blocks: object; +export interface UmbRichTextEditorValueType extends UmbBlockDataBaseValueType { markup: string; -}; +} /** * @element umb-property-editor-ui-tiny-mce @@ -18,11 +19,20 @@ type RichTextEditorValue = { export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements UmbPropertyEditorUiElement { #configuration?: UmbPropertyEditorConfigCollection; - @property({ type: Object }) - value?: RichTextEditorValue = { - blocks: {}, - markup: '', - }; + @property({ attribute: false }) + public set value(value: UmbRichTextEditorValueType | undefined) { + const buildUpValue: Partial = value ? { ...value } : {}; + buildUpValue.markup ??= ''; + buildUpValue.contentData ??= []; + buildUpValue.settingsData ??= []; + this._value = buildUpValue as UmbRichTextEditorValueType; + + this.#managerContext.setContents(buildUpValue.contentData); + this.#managerContext.setSettings(buildUpValue.settingsData); + } + public get value(): UmbRichTextEditorValueType { + return this._value; + } @property({ attribute: false }) public set config(config: UmbPropertyEditorConfigCollection | undefined) { @@ -32,9 +42,41 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements return this.#configuration; } + @state() + private _value: UmbRichTextEditorValueType = { + markup: '', + contentData: [], + settingsData: [], + }; + + #managerContext = new UmbBlockRteManagerContext(this); + #entriesContext = new UmbBlockRteEntriesContext(this); + + constructor() { + super(); + + this.observe(this.#entriesContext.layoutEntries, (layouts) => { + // Update manager: + this.#managerContext.setLayouts(layouts); + }); + + this.observe(this.#managerContext.contents, (contents) => { + this._value = { ...this._value, contentData: contents }; + this.#fireChangeEvent(); + }); + this.observe(this.#managerContext.settings, (settings) => { + this._value = { ...this._value, settingsData: settings }; + this.#fireChangeEvent(); + }); + } + + #fireChangeEvent() { + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + #onChange(event: InputEvent & { target: HTMLInputElement }) { this.value = { - blocks: {}, + ...this._value, markup: event.target.value, }; this.dispatchEvent(new UmbPropertyValueChangeEvent()); @@ -44,7 +86,7 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements return html` `;