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:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)[]) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user