Feature: RTE blocks validation (#18453)

* restructure block list code a bit for better readability

* improve error message

* block list validation

* validation for RTE Blocks

* rte blocks validation
This commit is contained in:
Niels Lyngsø
2025-02-26 14:10:59 +01:00
committed by GitHub
parent 207bd7283c
commit 07bb2d1840
13 changed files with 346 additions and 216 deletions

View File

@@ -22,7 +22,12 @@ import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
import '../../components/block-list-entry/index.js';
import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import { ExtractJsonQueryProps, UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
import {
extractJsonQueryProps,
UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
UmbFormControlMixin,
UmbValidationContext,
} from '@umbraco-cms/backoffice/validation';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs';
@@ -134,6 +139,12 @@ export class UmbPropertyEditorUIBlockListElement
}
#readonly = false;
@property({ type: Boolean })
mandatory?: boolean;
@property({ type: String })
mandatoryMessage?: string | undefined;
@state()
private _limitMin?: number;
@state()
@@ -155,37 +166,17 @@ export class UmbPropertyEditorUIBlockListElement
super();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
this.observe(
context.dataPath,
(dataPath) => {
// Translate paths for content/settings:
this.#contentDataPathTranslator?.destroy();
this.#settingsDataPathTranslator?.destroy();
if (dataPath) {
// Set the data path for the local validation context:
this.#validationContext.setDataPath(dataPath);
this.#gotPropertyContext(context);
});
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData');
}
},
'observeDataPath',
);
this.observe(
context?.alias,
(alias) => {
this.#managerContext.setPropertyAlias(alias);
},
'observePropertyAlias',
);
// Observe Blocks and clean up validation messages for content/settings that are not in the block list anymore:
this.observe(this.#managerContext.layouts, (layouts) => {
// Observe Blocks and clean up validation messages for content/settings that are not in the block list anymore:
this.observe(
this.#managerContext.layouts,
(layouts) => {
const contentKeys = layouts.map((x) => x.contentKey);
this.#validationContext.messages.getMessagesOfPathAndDescendant('$.contentData').forEach((message) => {
// get the KEY from this string: $.contentData[?(@.key == 'KEY')]
const key = ExtractJsonQueryProps(message.path).key;
const key = extractJsonQueryProps(message.path).key;
if (key && contentKeys.indexOf(key) === -1) {
this.#validationContext.messages.removeMessageByKey(message.key);
}
@@ -194,66 +185,14 @@ export class UmbPropertyEditorUIBlockListElement
const settingsKeys = layouts.map((x) => x.settingsKey).filter((x) => x !== undefined) as string[];
this.#validationContext.messages.getMessagesOfPathAndDescendant('$.settingsData').forEach((message) => {
// get the key from this string: $.settingsData[?(@.key == 'KEY')]
const key = ExtractJsonQueryProps(message.path).key;
const key = extractJsonQueryProps(message.path).key;
if (key && settingsKeys.indexOf(key) === -1) {
this.#validationContext.messages.removeMessageByKey(message.key);
}
});
});
this.observe(
observeMultiple([
this.#managerContext.layouts,
this.#managerContext.contents,
this.#managerContext.settings,
this.#managerContext.exposes,
]).pipe(debounceTime(20)),
([layouts, contents, settings, exposes]) => {
if (layouts.length === 0) {
super.value = undefined;
} else {
super.value = {
...super.value,
layout: { [UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts },
contentData: contents,
settingsData: settings,
expose: exposes,
};
}
// If we don't have a value set from the outside or an internal value, we don't want to set the value.
// This is added to prevent the block list from setting an empty value on startup.
if (this.#lastValue === undefined && super.value === undefined) {
return;
}
context.setValue(super.value);
},
'motherObserver',
);
// If the current property is readonly all inner block content should also be readonly.
this.observe(
observeMultiple([context.isReadOnly, context.variantId]),
([isReadOnly, variantId]) => {
const unique = 'UMB_PROPERTY_EDITOR_UI';
if (variantId === undefined) return;
if (isReadOnly) {
const state = {
unique,
variantId,
message: '',
};
this.#managerContext.readOnlyState.addState(state);
} else {
this.#managerContext.readOnlyState.removeState(unique);
}
},
'observeIsReadOnly',
);
});
},
null,
);
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
this.#managerContext.setVariantId(context.getVariantId());
@@ -272,25 +211,128 @@ export class UmbPropertyEditorUIBlockListElement
this.addValidator(
'rangeOverflow',
() => this.localize.term('validation_entriesExceed', this._limitMax, this.#entriesContext.getLength() - (this._limitMax || 0)),
() =>
this.localize.term(
'validation_entriesExceed',
this._limitMax,
this.#entriesContext.getLength() - (this._limitMax || 0),
),
() => !!this._limitMax && this.#entriesContext.getLength() > this._limitMax,
);
this.observe(this.#entriesContext.layoutEntries, (layouts) => {
this._layouts = layouts;
// Update sorter.
this.#sorter.setModel(layouts);
// Update manager:
this.#managerContext.setLayouts(layouts);
});
this.addValidator(
'valueMissing',
() => this.mandatoryMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
() => !!this.mandatory && this.#entriesContext.getLength() === 0,
);
this.observe(this.#managerContext.blockTypes, (blockTypes) => {
this._blocks = blockTypes;
});
this.observe(
this.#entriesContext.layoutEntries,
(layouts) => {
this._layouts = layouts;
// Update sorter.
this.#sorter.setModel(layouts);
// Update manager:
this.#managerContext.setLayouts(layouts);
},
null,
);
this.observe(this.#entriesContext.catalogueRouteBuilder, (routeBuilder) => {
this._catalogueRouteBuilder = routeBuilder;
});
this.observe(
this.#managerContext.blockTypes,
(blockTypes) => {
this._blocks = blockTypes;
},
null,
);
this.observe(
this.#entriesContext.catalogueRouteBuilder,
(routeBuilder) => {
this._catalogueRouteBuilder = routeBuilder;
},
null,
);
}
#gotPropertyContext(context: typeof UMB_PROPERTY_CONTEXT.TYPE) {
this.observe(
context.dataPath,
(dataPath) => {
// Translate paths for content/settings:
this.#contentDataPathTranslator?.destroy();
this.#settingsDataPathTranslator?.destroy();
if (dataPath) {
// Set the data path for the local validation context:
this.#validationContext.setDataPath(dataPath);
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData');
}
},
'observeDataPath',
);
this.observe(
context?.alias,
(alias) => {
this.#managerContext.setPropertyAlias(alias);
},
'observePropertyAlias',
);
this.observe(
observeMultiple([
this.#managerContext.layouts,
this.#managerContext.contents,
this.#managerContext.settings,
this.#managerContext.exposes,
]).pipe(debounceTime(20)),
([layouts, contents, settings, exposes]) => {
if (layouts.length === 0) {
super.value = undefined;
} else {
super.value = {
...super.value,
layout: { [UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts },
contentData: contents,
settingsData: settings,
expose: exposes,
};
}
// If we don't have a value set from the outside or an internal value, we don't want to set the value.
// This is added to prevent the block list from setting an empty value on startup.
if (this.#lastValue === undefined && super.value === undefined) {
return;
}
context.setValue(super.value);
},
'motherObserver',
);
// If the current property is readonly all inner block content should also be readonly.
this.observe(
observeMultiple([context.isReadOnly, context.variantId]),
([isReadOnly, variantId]) => {
const unique = 'UMB_PROPERTY_EDITOR_UI';
if (variantId === undefined) return;
if (isReadOnly) {
const state = {
unique,
variantId,
message: '',
};
this.#managerContext.readOnlyState.addState(state);
} else {
this.#managerContext.readOnlyState.removeState(unique);
}
},
'observeIsReadOnly',
);
}
protected override getFormElement() {

View File

@@ -6,7 +6,7 @@ import { UmbPropertyValueCloneController } from '@umbraco-cms/backoffice/propert
import { manifests } from './manifests';
import {
UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS,
type UmbPropertyEditorUiValueType,
type UmbPropertyEditorRteValueType,
} from '@umbraco-cms/backoffice/rte';
@customElement('umb-test-controller-host')
@@ -69,7 +69,7 @@ describe('UmbBlockRtePropertyValueCloner', () => {
},
};
const result = (await ctrl.clone(value)) as { value: UmbPropertyEditorUiValueType | undefined };
const result = (await ctrl.clone(value)) as { value: UmbPropertyEditorRteValueType | undefined };
const newContentKey = result.value?.blocks.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]?.[0].contentKey;
const newSettingsKey = result.value?.blocks.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]?.[0].settingsKey;

View File

@@ -1,15 +1,15 @@
import {
UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS,
type UmbPropertyEditorUiValueType,
type UmbPropertyEditorRteValueType,
} from '@umbraco-cms/backoffice/rte';
import type { UmbPropertyValueCloner } from '@umbraco-cms/backoffice/property';
import { UmbFlatLayoutBlockPropertyValueCloner } from '@umbraco-cms/backoffice/block';
export class UmbBlockRTEPropertyValueCloner implements UmbPropertyValueCloner<UmbPropertyEditorUiValueType> {
export class UmbBlockRTEPropertyValueCloner implements UmbPropertyValueCloner<UmbPropertyEditorRteValueType> {
#markup?: string;
#markupDoc?: Document;
async cloneValue(value: UmbPropertyEditorUiValueType) {
async cloneValue(value: UmbPropertyEditorRteValueType) {
if (value) {
this.#markup = value.markup;
@@ -19,7 +19,7 @@ export class UmbBlockRTEPropertyValueCloner implements UmbPropertyValueCloner<Um
const cloner = new UmbFlatLayoutBlockPropertyValueCloner(UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS, {
contentIdUpdatedCallback: this.#replaceContentKeyInMarkup,
});
const result = {} as UmbPropertyEditorUiValueType;
const result = {} as UmbPropertyEditorRteValueType;
result.blocks = await cloner.cloneValue(value.blocks);
result.markup = this.#markup;
return result;

View File

@@ -15,7 +15,7 @@ export class UmbBlockElementDataValidationPathTranslator extends UmbAbstractArra
const data = this._context.getTranslationData();
const entry = data[this.#propertyName][index];
if (!entry || !entry.key) {
console.error('block did not have key', this.#propertyName, index, data);
console.error('block did not have key', `${this.#propertyName}[${index}]`, entry);
return false;
}
return entry;

View File

@@ -7,11 +7,11 @@ const propValueRegex = /@\.([a-zA-Z_$][\w$]*)\s*==\s*['"]([^'"]*)['"]/g;
* @example
* ```ts
* const query = `?(@.culture == 'en-us' && @.segment == 'mySegment')`;
* const props = ExtractJsonQueryProps(query);
* const props = extractJsonQueryProps(query);
* console.log(props); // { culture: 'en-us', segment: 'mySegment' }
* ```
*/
export function ExtractJsonQueryProps(query: string): Record<string, string> {
export function extractJsonQueryProps(query: string): Record<string, string> {
// Object to hold property-value pairs
const propsMap: Record<string, string> = {};
let match;

View File

@@ -1,10 +1,10 @@
import { expect } from '@open-wc/testing';
import { ExtractJsonQueryProps } from './extract-json-query-properties.function.js';
import { extractJsonQueryProps } from './extract-json-query-properties.function.js';
describe('UmbJsonPathFunctions', () => {
it('retrieve property value', () => {
const query = `?(@.culture == 'en-us' && @.segment == 'mySegment')`;
const result = ExtractJsonQueryProps(query);
const result = extractJsonQueryProps(query);
expect(result.culture).to.eq('en-us');
expect(result.segment).to.eq('mySegment');

View File

@@ -1,4 +1,4 @@
import type { UmbPropertyEditorUiValueType } from '../types.js';
import type { UmbPropertyEditorRteValueType } from '../types.js';
import { UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../constants.js';
import { property, state } from '@umbraco-cms/backoffice/external/lit';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
@@ -11,8 +11,17 @@ import type {
UmbPropertyEditorUiElement,
UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
import {
UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
UmbFormControlMixin,
UmbValidationContext,
} from '@umbraco-cms/backoffice/validation';
import { UmbBlockElementDataValidationPathTranslator } from '@umbraco-cms/backoffice/block';
export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement implements UmbPropertyEditorUiElement {
export abstract class UmbPropertyEditorUiRteElementBase
extends UmbFormControlMixin<UmbPropertyEditorRteValueType | undefined, typeof UmbLitElement, undefined>(UmbLitElement)
implements UmbPropertyEditorUiElement
{
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
@@ -27,13 +36,13 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im
@property({
attribute: false,
type: Object,
hasChanged(value?: UmbPropertyEditorUiValueType, oldValue?: UmbPropertyEditorUiValueType) {
hasChanged(value?: UmbPropertyEditorRteValueType, oldValue?: UmbPropertyEditorRteValueType) {
return value?.markup !== oldValue?.markup;
},
})
public set value(value: UmbPropertyEditorUiValueType | undefined) {
public override set value(value: UmbPropertyEditorRteValueType | undefined) {
if (!value) {
this._value = undefined;
super.value = undefined;
this._markup = '';
this.#managerContext.setLayouts([]);
this.#managerContext.setContents([]);
@@ -42,18 +51,18 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im
return;
}
const buildUpValue: Partial<UmbPropertyEditorUiValueType> = value ? { ...value } : {};
const buildUpValue: Partial<UmbPropertyEditorRteValueType> = value ? { ...value } : {};
buildUpValue.markup ??= '';
buildUpValue.blocks ??= { layout: {}, contentData: [], settingsData: [], expose: [] };
buildUpValue.blocks.layout ??= {};
buildUpValue.blocks.contentData ??= [];
buildUpValue.blocks.settingsData ??= [];
buildUpValue.blocks.expose ??= [];
this._value = buildUpValue as UmbPropertyEditorUiValueType;
super.value = buildUpValue as UmbPropertyEditorRteValueType;
// Only update the actual editor markup if it is not the same as the value.
if (this._markup !== this._value.markup) {
this._markup = this._value.markup;
if (this._markup !== super.value.markup) {
this._markup = super.value.markup;
}
this.#managerContext.setLayouts(buildUpValue.blocks.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS] ?? []);
@@ -61,8 +70,8 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im
this.#managerContext.setSettings(buildUpValue.blocks.settingsData);
this.#managerContext.setExposes(buildUpValue.blocks.expose);
}
public get value() {
return this._value;
public override get value(): UmbPropertyEditorRteValueType | undefined {
return super.value;
}
/**
@@ -72,11 +81,25 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im
@property({ type: Boolean, reflect: true })
readonly = false;
@property({ type: Boolean })
mandatory?: boolean;
@property({ type: String })
mandatoryMessage?: string | undefined;
@state()
protected _config?: UmbPropertyEditorConfigCollection;
/**
* @deprecated _value is depreacated, use `super.value` instead.
*/
@state()
protected _value?: UmbPropertyEditorUiValueType | undefined;
protected get _value(): UmbPropertyEditorRteValueType | undefined {
return super.value;
}
protected set _value(value: UmbPropertyEditorRteValueType | undefined) {
super.value = value;
}
/**
* Separate state for markup, to avoid re-rendering/re-setting the value of the Tiptap editor when the value does not really change.
@@ -87,85 +110,107 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im
readonly #managerContext = new UmbBlockRteManagerContext(this);
readonly #entriesContext = new UmbBlockRteEntriesContext(this);
readonly #validationContext = new UmbValidationContext(this);
#contentDataPathTranslator?: UmbBlockElementDataValidationPathTranslator;
#settingsDataPathTranslator?: UmbBlockElementDataValidationPathTranslator;
constructor() {
super();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
// TODO: Implement validation translation for RTE Blocks:
/*
this.observe(
context.dataPath,
(dataPath) => {
// Translate paths for content/settings:
this.#contentDataPathTranslator?.destroy();
this.#settingsDataPathTranslator?.destroy();
if (dataPath) {
// Set the data path for the local validation context:
this.#validationContext.setDataPath(dataPath);
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData');
}
},
'observeDataPath',
);
*/
this.observe(
context?.alias,
(alias) => {
this.#managerContext.setPropertyAlias(alias);
},
'observePropertyAlias',
);
this.observe(this.#entriesContext.layoutEntries, (layouts) => {
// Update manager:
this.#managerContext.setLayouts(layouts);
});
this.observe(
observeMultiple([
this.#managerContext.layouts,
this.#managerContext.contents,
this.#managerContext.settings,
this.#managerContext.exposes,
]),
([layouts, contents, settings, exposes]) => {
if (layouts.length === 0) {
this._value = undefined;
} else {
this._value = {
markup: this._markup,
blocks: {
layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts },
contentData: contents,
settingsData: settings,
expose: exposes,
},
};
}
// If we don't have a value set from the outside or an internal value, we don't want to set the value.
// This is added to prevent the block list from setting an empty value on startup.
if (this._value?.markup === undefined) {
return;
}
context.setValue(this._value);
},
'motherObserver',
);
this.#gotPropertyContext(context);
});
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
this.#managerContext.setVariantId(context.getVariantId());
});
this.observe(this.#entriesContext.layoutEntries, (layouts) => {
// Update manager:
this.#managerContext.setLayouts(layouts);
});
this.observe(
this.#entriesContext.layoutEntries,
(layouts) => {
// Update manager:
this.#managerContext.setLayouts(layouts);
},
null,
);
this.addValidator(
'valueMissing',
() => this.mandatoryMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
() => !!this.mandatory && this.value === undefined,
);
}
#gotPropertyContext(context: typeof UMB_PROPERTY_CONTEXT.TYPE) {
this.observe(
context.dataPath,
(dataPath) => {
// Translate paths for content/settings:
this.#contentDataPathTranslator?.destroy();
this.#settingsDataPathTranslator?.destroy();
if (dataPath) {
// Set the data path for the local validation context:
this.#validationContext.setDataPath(dataPath + '.blocks');
this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData');
this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData');
}
},
'observeDataPath',
);
this.observe(
context?.alias,
(alias) => {
this.#managerContext.setPropertyAlias(alias);
},
'observePropertyAlias',
);
this.observe(
observeMultiple([
this.#managerContext.layouts,
this.#managerContext.contents,
this.#managerContext.settings,
this.#managerContext.exposes,
]),
([layouts, contents, settings, exposes]) => {
if (layouts.length === 0) {
if (super.value?.markup === undefined) {
super.value = undefined;
} else {
super.value = {
...super.value,
blocks: {
layout: {},
contentData: [],
settingsData: [],
expose: [],
},
};
}
} else {
super.value = {
markup: this._markup,
blocks: {
layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts },
contentData: contents,
settingsData: settings,
expose: exposes,
},
};
}
// If we don't have a value set from the outside or an internal value, we don't want to set the value.
// This is added to prevent the block list from setting an empty value on startup.
if (super.value?.markup === undefined) {
return;
}
context.setValue(super.value);
},
'motherObserver',
);
}
protected _filterUnusedBlocks(usedContentKeys: (string | null)[]) {

View File

@@ -1,4 +1,4 @@
import type { UmbPropertyEditorUiValueType } from '../types.js';
import type { UmbPropertyEditorRteValueType } from '../types.js';
import {
UmbBlockValueResolver,
type UmbBlockDataValueModel,
@@ -6,9 +6,9 @@ import {
} from '@umbraco-cms/backoffice/block';
import type { UmbElementValueModel } from '@umbraco-cms/backoffice/content';
export class UmbRteBlockValueResolver extends UmbBlockValueResolver<UmbPropertyEditorUiValueType> {
export class UmbRteBlockValueResolver extends UmbBlockValueResolver<UmbPropertyEditorRteValueType> {
async processValues(
property: UmbElementValueModel<UmbPropertyEditorUiValueType>,
property: UmbElementValueModel<UmbPropertyEditorRteValueType>,
valuesCallback: (values: Array<UmbBlockDataValueModel>) => Promise<Array<UmbBlockDataValueModel> | undefined>,
) {
if (property.value) {
@@ -24,7 +24,7 @@ export class UmbRteBlockValueResolver extends UmbBlockValueResolver<UmbPropertyE
}
async processVariants(
property: UmbElementValueModel<UmbPropertyEditorUiValueType>,
property: UmbElementValueModel<UmbPropertyEditorRteValueType>,
variantsCallback: (values: Array<UmbBlockExposeModel>) => Promise<Array<UmbBlockExposeModel> | undefined>,
) {
if (property.value) {

View File

@@ -1,7 +1,13 @@
import type { UmbBlockValueType } from '@umbraco-cms/backoffice/block';
import type { UmbBlockRteLayoutModel } from '@umbraco-cms/backoffice/block-rte';
export interface UmbPropertyEditorUiValueType {
export interface UmbPropertyEditorRteValueType {
markup: string;
blocks: UmbBlockValueType<UmbBlockRteLayoutModel>;
}
/**
* @deprecated Use `UmbPropertyEditorRteValueType` instead, will be removed in v.17.0.0
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UmbPropertyEditorUiValueType extends UmbPropertyEditorRteValueType {}

View File

@@ -38,21 +38,14 @@ export class UmbPropertyEditorUITinyMceElement extends UmbPropertyEditorUiRteEle
blockElement.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY),
);
this._filterUnusedBlocks(usedContentKeys);
// Then get the content of the editor and update the value.
// maybe in this way doc.body.innerHTML;
this._markup = markup;
if (this.value) {
this.value = {
...this.value,
markup: this._markup,
if (super.value) {
super.value = {
...super.value,
markup: markup,
};
} else {
this.value = {
markup: this._markup,
markup: markup,
blocks: {
layout: {},
contentData: [],
@@ -62,6 +55,9 @@ export class UmbPropertyEditorUITinyMceElement extends UmbPropertyEditorUiRteEle
};
}
// lets run this one after we set the value, to make sure we don't reset the value.
this._filterUnusedBlocks(usedContentKeys);
this._fireChangeEvent();
}

View File

@@ -35,6 +35,16 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
@property({ attribute: false })
configuration?: UmbPropertyEditorConfigCollection;
/**
* Sets the input to required, meaning validation will fail if the value is empty.
* @type {boolean}
*/
@property({ type: Boolean })
required?: boolean;
@property({ type: String })
requiredMessage?: string;
/**
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
*/
@@ -53,6 +63,16 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
@state()
_toolbar: UmbTiptapToolbarValue = [[[]]];
constructor() {
super();
this.addValidator(
'valueMissing',
() => this.requiredMessage ?? 'Value is required',
() => !!this.required && this.isEmpty(),
);
}
protected override async firstUpdated() {
await Promise.all([await this.#loadExtensions(), await this.#loadEditor()]);
}
@@ -131,6 +151,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
},
onUpdate: ({ editor }) => {
this.#value = editor.getHTML();
this._runValidators();
this.dispatchEvent(new UmbChangeEvent());
},
});
@@ -186,6 +207,15 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
justify-content: center;
}
:host(:not([pristine]):invalid),
/* polyfill support */
:host(:not([pristine])[internals-invalid]) {
--umb-tiptap-edge-border-color: var(--uui-color-danger);
#editor {
border-color: var(--uui-color-danger);
}
}
#editor {
/* Required as overflow is set to auto, so that the scrollbars don't appear. */
display: flex;

View File

@@ -90,6 +90,10 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
border-left-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
border-right-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
background-color: var(--uui-color-surface);
color: var(--color-text);
font-size: var(--uui-type-default-size);

View File

@@ -1,6 +1,6 @@
import type { UmbInputTiptapElement } from '../../components/input-tiptap/input-tiptap.element.js';
import { UmbPropertyEditorUiRteElementBase } from '@umbraco-cms/backoffice/rte';
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { customElement, html, type PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import '../../components/input-tiptap/input-tiptap.element.js';
@@ -9,9 +9,15 @@ import '../../components/input-tiptap/input-tiptap.element.js';
*/
@customElement('umb-property-editor-ui-tiptap')
export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElementBase {
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.firstUpdated(_changedProperties);
this.addFormControlElement(this.shadowRoot?.querySelector('umb-input-tiptap') as UmbInputTiptapElement);
}
#onChange(event: CustomEvent & { target: UmbInputTiptapElement }) {
const tipTapElement = event.target;
const value = tipTapElement.value;
const markup = tipTapElement.value;
// If we don't get any markup clear the property editor value.
if (tipTapElement.isEmpty()) {
@@ -28,24 +34,20 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem
/<umb-rte-block(?:-inline)?(?: class="(?:.[^"]*)")? data-content-key="(?<key>.[^"]*)">(?:<!--Umbraco-Block-->)?<\/umb-rte-block(?:-inline)?>/gi,
);
let blockElement: RegExpExecArray | null;
while ((blockElement = regex.exec(value)) !== null) {
while ((blockElement = regex.exec(markup)) !== null) {
if (blockElement.groups?.key) {
usedContentKeys.push(blockElement.groups.key);
}
}
this._filterUnusedBlocks(usedContentKeys);
this._markup = value;
if (this.value) {
this.value = {
...this.value,
markup: this._markup,
markup: markup,
};
} else {
this.value = {
markup: this._markup,
markup: markup,
blocks: {
layout: {},
contentData: [],
@@ -55,6 +57,9 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem
};
}
// lets run this one after we set the value, to make sure we don't reset the value.
this._filterUnusedBlocks(usedContentKeys);
this._fireChangeEvent();
}
@@ -64,6 +69,8 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem
.configuration=${this._config}
.value=${this._markup}
?readonly=${this.readonly}
?required=${this.mandatory}
?required-message=${this.mandatoryMessage}
@change=${this.#onChange}></umb-input-tiptap>
`;
}