* fix multiple text string validation

* notify about messages

* cherry picked fix

* protection again unnecessary calls

* json path cherry pick + tests

* validation message change lock

* cherry pick from control lifecycle

* optimization

* propagate errors

* cherry picked sync

* query umb-input-multiple-text-string

* remove unused import

* remove optional chain expression

* use !

* outcomment the error handling

* outcomment more promise rejection error

* Fixed issue with multi URL picker.

* remove unesecary warning

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Niels Lyngsø
2025-03-20 17:22:05 +01:00
committed by GitHub
parent f7854b8c95
commit 131c9cda6f
14 changed files with 203 additions and 148 deletions

View File

@@ -374,8 +374,6 @@ export default {
fileSecurityValidationFailure: 'One or more file security validations have failed',
moveToSameFolderFailed: 'Parent and destination folders cannot be the same',
uploadNotAllowed: 'Upload is not allowed in this location.',
noticeExtensionsServerOverride:
'Regardless of the allowed file types, the following limitations apply system-wide due to the server configuration:',
},
member: {
'2fa': 'Two-Factor Authentication',
@@ -899,7 +897,6 @@ export default {
retrieve: 'Retrieve',
retry: 'Retry',
rights: 'Permissions',
serverConfiguration: 'Server Configuration',
scheduledPublishing: 'Scheduled Publishing',
umbracoInfo: 'Umbraco info',
search: 'Search',

View File

@@ -48,8 +48,9 @@ export class UmbContextProviderController<
public override destroy(): void {
if (this.#host) {
this.#host.removeUmbController(this);
const host = this.#host;
(this.#host as unknown) = undefined;
host.removeUmbController(this);
}
super.destroy();
}

View File

@@ -173,12 +173,13 @@ export class UmbPropertyEditorUIBlockListElement
this.observe(
this.#managerContext.layouts,
(layouts) => {
const validationMessagesToRemove: string[] = [];
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;
if (key && contentKeys.indexOf(key) === -1) {
this.#validationContext.messages.removeMessageByKey(message.key);
validationMessagesToRemove.push(message.key);
}
});
@@ -187,9 +188,12 @@ export class UmbPropertyEditorUIBlockListElement
// get the key from this string: $.settingsData[?(@.key == 'KEY')]
const key = extractJsonQueryProps(message.path).key;
if (key && settingsKeys.indexOf(key) === -1) {
this.#validationContext.messages.removeMessageByKey(message.key);
validationMessagesToRemove.push(message.key);
}
});
// Remove the messages after the loop to prevent changing the array while iterating over it.
this.#validationContext.messages.removeMessageByKeys(validationMessagesToRemove);
},
null,
);

View File

@@ -33,6 +33,26 @@ export class UmbValidationMessagesManager {
this.messages.subscribe((x) => console.log(logName, x));
}
getMessages(): Array<UmbValidationMessage> {
return this.#messages.getValue();
}
#updateLock = 0;
initiateChange() {
this.#updateLock++;
this.#messages.mute();
// TODO: When ready enable this code will enable handling a finish automatically by this implementation 'using myState.initiatePropertyValueChange()' (Relies on TS support of Using) [NL]
/*return {
[Symbol.dispose]: this.finishPropertyValueChange,
};*/
}
finishChange() {
this.#updateLock--;
if (this.#updateLock === 0) {
this.#messages.unmute();
}
}
getHasAnyMessages(): boolean {
return this.#messages.getValue().length !== 0;
}
@@ -75,7 +95,9 @@ export class UmbValidationMessagesManager {
if (this.#messages.getValue().find((x) => x.type === type && x.path === path && x.body === body)) {
return;
}
this.initiateChange();
this.#messages.appendOne({ type, key, path, body: body });
this.finishChange();
}
addMessages(type: UmbValidationMessageType, path: string, bodies: Array<string>): void {
@@ -86,27 +108,42 @@ export class UmbValidationMessagesManager {
const newBodies = bodies.filter(
(message) => existingMessages.find((x) => x.type === type && x.path === path && x.body === message) === undefined,
);
this.initiateChange();
this.#messages.append(newBodies.map((body) => ({ type, key: UmbId.new(), path, body })));
this.finishChange();
}
removeMessageByKey(key: string): void {
this.initiateChange();
this.#messages.removeOne(key);
this.finishChange();
}
removeMessageByKeys(keys: Array<string>): void {
if (keys.length === 0) return;
this.initiateChange();
this.#messages.filter((x) => keys.indexOf(x.key) === -1);
this.finishChange();
}
removeMessagesByType(type: UmbValidationMessageType): void {
this.initiateChange();
this.#messages.filter((x) => x.type !== type);
this.finishChange();
}
removeMessagesByPath(path: string): void {
this.initiateChange();
this.#messages.filter((x) => x.path !== path);
this.finishChange();
}
removeMessagesAndDescendantsByPath(path: string): void {
this.initiateChange();
this.#messages.filter((x) => MatchPathOrDescendantPath(x.path, path));
this.finishChange();
}
removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void {
//path = path.toLowerCase();
this.initiateChange();
this.#messages.filter((x) => !(x.type === type && x.path === path));
this.finishChange();
}
#translatePath(path: string): string | undefined {
@@ -124,6 +161,7 @@ export class UmbValidationMessagesManager {
#translators: Array<UmbValidationMessageTranslator> = [];
addTranslator(translator: UmbValidationMessageTranslator): void {
this.initiateChange();
if (this.#translators.indexOf(translator) === -1) {
this.#translators.push(translator);
}
@@ -137,6 +175,7 @@ export class UmbValidationMessagesManager {
this.#messages.updateOne(msg.key, { path: newPath });
}
}
this.finishChange();
}
removeTranslator(translator: UmbValidationMessageTranslator): void {

View File

@@ -35,8 +35,6 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
}
});
this.#control = formControl;
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
}
get isValid(): boolean {
@@ -82,12 +80,18 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
override hostConnected(): void {
super.hostConnected();
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
if (this.#context) {
this.#context.addValidator(this);
}
}
override hostDisconnected(): void {
super.hostDisconnected();
if (this.#control) {
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
}
if (this.#context) {
this.#context.removeValidator(this);
// Remove any messages that this validator has added:
@@ -99,11 +103,9 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
}
override destroy(): void {
super.destroy();
if (this.#control) {
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
this.#control = undefined as any;
}
super.destroy();
}
}

View File

@@ -122,11 +122,12 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
this.observe(
parent.messages.messagesOfPathAndDescendant(dataPath),
(msgs) => {
this.messages.initiateChange();
//this.messages.appendMessages(msgs);
if (this.#parentMessages) {
// Remove the local messages that does not exist in the parent anymore:
const toRemove = this.#parentMessages.filter((msg) => !msgs.find((m) => m.key === msg.key));
this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key));
this.messages.removeMessageByKeys(toRemove.map((msg) => msg.key));
}
this.#parentMessages = msgs;
msgs.forEach((msg) => {
@@ -139,6 +140,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
// Notice, the local message uses the same key. [NL]
this.messages.addMessage(msg.type, path, msg.body, msg.key);
});
this.messages.finishChange();
},
'observeParentMessages',
);
@@ -147,6 +149,9 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
this.messages.messages,
(msgs) => {
if (!this.#parent) return;
this.#parent!.messages.initiateChange();
//this.messages.appendMessages(msgs);
if (this.#localMessages) {
// Remove the parent messages that does not exist locally anymore:
@@ -165,6 +170,8 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
// Notice, the parent message uses the same key. [NL]
this.#parent!.messages.addMessage(msg.type, path, msg.body, msg.key);
});
this.#parent!.messages.finishChange();
},
'observeLocalMessages',
);
@@ -172,6 +179,19 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
// Notice skipHost ^^, this is because we do not want it to consume it self, as this would be a match for this consumption, instead we will look at the parent and above. [NL]
}
override hostConnected(): void {
super.hostConnected();
if (this.#parent) {
this.#parent.addValidator(this);
}
}
override hostDisconnected(): void {
super.hostDisconnected();
if (this.#parent) {
this.#parent.removeValidator(this);
}
}
/**
* Get if this context is valid.
* Notice this does not verify the validity.
@@ -229,18 +249,27 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
() => false,
);
if (!this.messages) {
/*if (this.#validators.length === 0 && resultsStatus === false) {
throw new Error('No validators to validate, but validation failed');
}*/
if (this.messages === undefined) {
// This Context has been destroyed while is was validating, so we should not continue.
return Promise.reject();
}
const hasMessages = this.messages.getHasAnyMessages();
// If we have any messages then we are not valid, otherwise lets check the validation results: [NL]
// This enables us to keep client validations though UI is not present anymore — because the client validations got defined as messages. [NL]
const isValid = this.messages.getHasAnyMessages() ? false : resultsStatus;
const isValid = hasMessages ? false : resultsStatus;
this.#isValid = isValid;
if (isValid === false) {
/*if (hasMessages === false && resultsStatus === false) {
throw new Error('Missing validation messages to represent why a child validation context is invalid.');
}*/
// Focus first invalid element:
this.focusFirstInvalidElement();
return Promise.reject();
@@ -278,6 +307,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
}
override destroy(): void {
this.#providerCtrl?.destroy();
this.#providerCtrl = undefined;
if (this.#parent) {
this.#parent.removeValidator(this);

View File

@@ -4,8 +4,13 @@
* @param {string} path - the JSON path to the value that should be found
* @returns {unknown} - the found value.
*/
export function GetValueByJsonPath(data: unknown, path: string): unknown {
export function GetValueByJsonPath<ReturnType = unknown>(data: unknown, path: string): ReturnType | undefined {
if (path === '$') return data as ReturnType;
// strip $ from the path:
if (path.startsWith('$[')) {
return _GetNextArrayEntryFromPath(data as Array<unknown>, path.slice(2));
}
const strippedPath = path.startsWith('$.') ? path.slice(2) : path;
// get value from the path:
return GetNextPropertyValueFromPath(data, strippedPath);
@@ -33,49 +38,62 @@ function GetNextPropertyValueFromPath(data: any, path: string): any {
const value = data[key];
// if there is no rest of the path, return the value:
if (rest === undefined) return value;
// if the value is an array, get the value at the index:
if (Array.isArray(value)) {
// get the value until the next ']', the value can be anything in between the brackets:
const lookupEnd = rest.match(/\]/);
if (!lookupEnd) return undefined;
// get everything before the match:
const entryPointer = rest.slice(0, lookupEnd.index);
// check if the entryPointer is a JSON Path Filter ( starting with ?( and ending with ) ):
if (entryPointer.startsWith('?(') && entryPointer.endsWith(')')) {
// get the filter from the entryPointer:
// get the filter as a function:
const jsFilter = JsFilterFromJsonPathFilter(entryPointer);
// find the index of the value that matches the filter:
const index = value.findIndex(jsFilter[0]);
// if the index is -1, return undefined:
if (index === -1) return undefined;
// get the value at the index:
const data = value[index];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= rest.length) {
return data;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(data, rest.slice(lookupEnd.index + 2)) ?? data;
} else {
// get the value at the index:
const indexAsNumber = parseInt(entryPointer);
if (isNaN(indexAsNumber)) return undefined;
const data = value[indexAsNumber];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= rest.length) {
return data;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(data, rest.slice(lookupEnd.index + 2)) ?? data;
}
return _GetNextArrayEntryFromPath(value, rest);
} else {
// continue with the rest of the path:
return GetNextPropertyValueFromPath(value, rest);
}
}
/**
* @private
* @param {object} array - object to traverse for the value.
* @param {string} path - the JSON path to the value that should be found, notice without the starting '['
* @returns {unknown} - the found value.
*/
function _GetNextArrayEntryFromPath(array: Array<any>, path: string): any {
if (!array) return undefined;
// get the value until the next ']', the value can be anything in between the brackets:
const lookupEnd = path.match(/\]/);
if (!lookupEnd) return undefined;
// get everything before the match:
const entryPointer = path.slice(0, lookupEnd.index);
// check if the entryPointer is a JSON Path Filter ( starting with ?( and ending with ) ):
if (entryPointer.startsWith('?(') && entryPointer.endsWith(')')) {
// get the filter from the entryPointer:
// get the filter as a function:
const jsFilter = JsFilterFromJsonPathFilter(entryPointer);
// find the index of the value that matches the filter:
const index = array.findIndex(jsFilter[0]);
// if the index is -1, return undefined:
if (index === -1) return undefined;
// get the value at the index:
const entryData = array[index];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= path.length) {
return entryData;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(entryData, path.slice(lookupEnd.index + 2)) ?? entryData;
} else {
// get the value at the index:
const indexAsNumber = parseInt(entryPointer);
if (isNaN(indexAsNumber)) return undefined;
const entryData = array[indexAsNumber];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= path.length) {
return entryData;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(entryData, path.slice(lookupEnd.index + 2)) ?? entryData;
}
}
/**
* @param {string} filter - A JSON Query, limited to filtering features. Do not support other JSON PATH Query features.
* @returns {Array<(queryFilter: any) => boolean>} - An array of methods that returns true if the given items property value matches the value of the query.

View File

@@ -2,6 +2,14 @@ import { expect } from '@open-wc/testing';
import { GetValueByJsonPath } from './json-path.function.js';
describe('UmbJsonPathFunctions', () => {
it('retrieves root when path is root', () => {
const data = { value: 'test' };
const result = GetValueByJsonPath(data, '$') as any;
expect(result).to.eq(data);
expect(result.value).to.eq('test');
});
it('retrieve property value', () => {
const result = GetValueByJsonPath({ value: 'test' }, '$.value');
@@ -34,4 +42,10 @@ describe('UmbJsonPathFunctions', () => {
expect(result).to.eq('test');
});
it('query of array in root', () => {
const result = GetValueByJsonPath([{ id: '123', value: 'test' }], "$[?(@.id == '123')].value");
expect(result).to.eq('test');
});
});

View File

@@ -19,4 +19,10 @@ describe('ReplaceStartOfPath', () => {
expect(result).to.eq('$');
});
it('replaces the root character with root character', () => {
const result = ReplaceStartOfPath('$.start.test', '$', '$');
expect(result).to.eq('$.start.test');
});
});

View File

@@ -74,7 +74,6 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
* @returns Promise that resolves to void when the validation is complete.
*/
public async validate(): Promise<Array<void>> {
//return this.validation.validate();
return Promise.all(this.#validationContexts.map((context) => context.validate()));
}
@@ -97,7 +96,15 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
async () => {
onValid().then(this.#completeSubmit, this.#rejectSubmit);
},
async () => {
async (/*error*/) => {
/*if (error) {
throw new Error(error);
}*/
// TODO: Implement developer-mode logging here. [NL]
console.warn(
'Validation failed because of these validation messages still begin present: ',
this.#validationContexts.flatMap((x) => x.messages.getMessages()),
);
onInvalid().then(this.#resolveSubmit, this.#rejectSubmit);
},
);
@@ -105,7 +112,10 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
return this.#submitPromise;
}
#rejectSubmit = () => {
#rejectSubmit = (/*error: any*/) => {
/*if (error) {
throw new Error(error);
}*/
if (this.#submitPromise) {
// TODO: Capture the validation contexts messages on open, and then reset to them in this case. [NL]

View File

@@ -11,15 +11,16 @@ import type {
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
import '../components/input-multi-url/index.js';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
/**
* @element umb-property-editor-ui-multi-url-picker
*/
@customElement('umb-property-editor-ui-multi-url-picker')
export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Array })
value: Array<UmbLinkPickerLink> = [];
export class UmbPropertyEditorUIMultiUrlPickerElement
extends UmbFormControlMixin<Array<UmbLinkPickerLink>, typeof UmbLitElement, undefined>(UmbLitElement)
implements UmbPropertyEditorUiElement
{
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
@@ -81,6 +82,7 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl
this,
);
}
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-multi-url')!);
}
#onChange(event: CustomEvent & { target: UmbInputMultiUrlElement }) {

View File

@@ -1,6 +1,5 @@
import { UmbPropertyEditorUIMultipleTextStringElement } from '../multiple-text-string/property-editor-ui-multiple-text-string.element.js';
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
import { formatBytes } from '@umbraco-cms/backoffice/utils';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
import type { UmbTemporaryFileConfigurationModel } from '@umbraco-cms/backoffice/temporary-file';
@@ -40,7 +39,8 @@ export class UmbPropertyEditorUIAcceptedUploadTypesElement
}
#addValidators(config: UmbTemporaryFileConfigurationModel) {
this._inputElement?.addValidator(
const inputElement = this.shadowRoot?.querySelector('umb-input-multiple-text-string');
inputElement?.addValidator(
'badInput',
() => {
let message = this.localize.term('validation_invalidExtensions');
@@ -53,7 +53,7 @@ export class UmbPropertyEditorUIAcceptedUploadTypesElement
return message;
},
() => {
const extensions = this._inputElement?.items;
const extensions = inputElement?.items;
if (!extensions) return false;
if (
config.allowedUploadedFileExtensions.length &&
@@ -69,49 +69,8 @@ export class UmbPropertyEditorUIAcceptedUploadTypesElement
);
}
#renderAcceptedTypes() {
if (!this._acceptedTypes.length && !this._disallowedTypes.length && !this._maxFileSize) {
return nothing;
}
return html`
<uui-box id="notice" headline=${this.localize.term('general_serverConfiguration')}>
<p><umb-localize key="media_noticeExtensionsServerOverride"></umb-localize></p>
${when(
this._acceptedTypes.length,
() => html`
<p>
<umb-localize key="validation_allowedExtensions"></umb-localize>
<strong>${this._acceptedTypes.join(', ')}</strong>
</p>
`,
)}
${when(
this._disallowedTypes.length,
() => html`
<p>
<umb-localize key="validation_disallowedExtensions"></umb-localize>
<strong>${this._disallowedTypes.join(', ')}</strong>
</p>
`,
)}
${when(
this._maxFileSize,
() => html`
<p>
${this.localize.term('media_maxFileSize')}
<strong title="${this.localize.number(this._maxFileSize!)} bytes"
>${formatBytes(this._maxFileSize!, { decimals: 2 })}</strong
>.
</p>
`,
)}
</uui-box>
`;
}
override render() {
return html`${this.#renderAcceptedTypes()} ${super.render()}`;
return html`${super.render()}`;
}
static override readonly styles = [

View File

@@ -1,12 +1,8 @@
import { customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit';
import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { umbBindToValidation, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import {
UMB_SUBMITTABLE_WORKSPACE_CONTEXT,
UmbSubmittableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/components';
import type {
@@ -18,10 +14,10 @@ import type {
* @element umb-property-editor-ui-multiple-text-string
*/
@customElement('umb-property-editor-ui-multiple-text-string')
export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Array })
value?: Array<string>;
export class UmbPropertyEditorUIMultipleTextStringElement
extends UmbFormControlMixin<Array<string>, typeof UmbLitElement, undefined>(UmbLitElement)
implements UmbPropertyEditorUiElement
{
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
@@ -65,23 +61,12 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
@state()
private _max = Infinity;
@query('#input', true)
protected _inputElement?: UmbInputMultipleTextStringElement;
protected _validationContext = new UmbValidationContext(this);
constructor() {
super();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
this._label = context.getLabel();
});
this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (context) => {
if (context instanceof UmbSubmittableWorkspaceContextBase) {
context.addValidationContext(this._validationContext);
}
});
}
protected override firstUpdated() {
@@ -91,6 +76,7 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
this,
);
}
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-multiple-text-string')!);
}
#onChange(event: UmbChangeEvent) {
@@ -100,31 +86,18 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
// Prevent valid events from bubbling outside the message element
#onValid(event: Event) {
event.stopPropagation();
}
// Prevent invalid events from bubbling outside the message element
#onInvalid(event: Event) {
event.stopPropagation();
}
override render() {
return html`
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
<umb-input-multiple-text-string
id="input"
max=${this._max}
min=${this._min}
.items=${this.value ?? []}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
@change=${this.#onChange}
${umbBindToValidation(this)}>
</umb-input-multiple-text-string>
</umb-form-validation-message>
<umb-input-multiple-text-string
max=${this._max}
min=${this._min}
.items=${this.value ?? []}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
@change=${this.#onChange}
${umbBindToValidation(this)}>
</umb-input-multiple-text-string>
`;
}
}

View File

@@ -12,7 +12,7 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.firstUpdated(_changedProperties);
this.addFormControlElement(this.shadowRoot?.querySelector('umb-input-tiptap') as UmbInputTiptapElement);
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-tiptap') as UmbInputTiptapElement);
}
#onChange(event: CustomEvent & { target: UmbInputTiptapElement }) {