diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 1918a0695249..048c3e837789 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2228,7 +2228,8 @@ export default { legacyOptionDescription: 'This option is no longer supported, please select something else', numberMinimum: "Value must be greater than or equal to '%0%'.", numberMaximum: "Value must be less than or equal to '%0%'.", - numberMisconfigured: "Minimum value '%0%'must be less than the maximum value '%1%'.", + numberMisconfigured: "Minimum value '%0%' must be less than the maximum value '%1%'.", + rangeExceeds: 'The low value must not exceed the high value.', invalidExtensions: 'One or more of the extensions are invalid.', allowedExtensions: 'Allowed extensions are:', disallowedExtensions: 'Disallowed extensions are:', diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts index 018ffe8921d2..9da04cd56373 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts @@ -114,6 +114,15 @@ export class UmbLocalizationController(key: K, ...args: FunctionParams): string { if (!this.#usedKeys.includes(key)) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts index 9ed435e8980e..41d300a752df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts @@ -93,7 +93,7 @@ export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElemen this.addValidator( 'patternMismatch', () => { - return 'The low value must not be exceed the high value'; + return '#validation_rangeExceeds'; }, () => { return this._minValue !== undefined && this._maxValue !== undefined ? this._minValue > this._maxValue : false; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts index 12787012c0a0..0cf5d6dcd2c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts @@ -1,11 +1,42 @@ import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UUISliderEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbFormControlMixin } from '../../validation/mixins/index.js'; + +function splitString(value: string | undefined): Partial<[number | undefined, number | undefined]> { + const [from, to] = (value ?? ',').split(','); + const fromNumber = makeNumberOrUndefined(from); + return [fromNumber, makeNumberOrUndefined(to, fromNumber)]; +} + +function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | number) { + if (value === undefined) { + return fallback; + } + const n = Number(value); + if (isNaN(n)) { + return fallback; + } + return n; +} + +function undefinedFallbackToString(value: number | undefined, fallback: number): string { + return (value === undefined ? fallback : value).toString(); +} @customElement('umb-input-slider') -export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '') { +export class UmbInputSliderElement extends UmbFormControlMixin(UmbLitElement, '') { + override set value(value: string) { + const [from, to] = splitString(value); + this.#valueLow = from; + this.#valueHigh = to; + super.value = value; + } + override get value() { + return super.value; + } + @property() label: string = ''; @@ -19,10 +50,32 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '' step = 1; @property({ type: Number }) - valueLow = 0; + public get valueLow(): number | undefined { + return this.#valueLow; + } + public set valueLow(value: number | undefined) { + this.#valueLow = value; + this.#setValueFromLowHigh(); + } + #valueLow?: number | undefined; @property({ type: Number }) - valueHigh = 0; + public get valueHigh(): number | undefined { + return this.#valueHigh; + } + public set valueHigh(value: number | undefined) { + this.#valueHigh = value; + this.#setValueFromLowHigh(); + } + #valueHigh?: number | undefined; + + #setValueFromLowHigh() { + if (this.enableRange) { + super.value = `${undefinedFallbackToString(this.valueLow, this.min)},${undefinedFallbackToString(this.valueHigh, this.max)}`; + } else { + super.value = `${undefinedFallbackToString(this.valueLow, this.min)}`; + } + } @property({ type: Boolean, attribute: 'enable-range' }) enableRange = false; @@ -36,6 +89,62 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '' @property({ type: Boolean, reflect: true }) readonly = false; + constructor() { + super(); + + this.addValidator( + 'rangeUnderflow', + () => { + return this.localize.term('validation_numberMinimum', [this.min?.toString()]); + }, + () => { + if (this.min !== undefined) { + const [from, to] = splitString(this.value); + if (to !== undefined && to < this.min) { + return true; + } + if (from !== undefined && from < this.min) { + return true; + } + } + return false; + }, + ); + + this.addValidator( + 'rangeOverflow', + () => { + return this.localize.term('validation_numberMaximum', [this.max?.toString()]); + }, + () => { + if (this.max !== undefined) { + const [from, to] = splitString(this.value); + if (to !== undefined && to > this.max) { + return true; + } + if (from !== undefined && from > this.max) { + return true; + } + } + return false; + }, + ); + + this.addValidator( + 'patternMismatch', + () => { + return this.localize.term('validation_rangeExceeds'); + }, + () => { + const [from, to] = splitString(this.value); + if (to !== undefined && from !== undefined) { + return from > to; + } + return false; + }, + ); + } + protected override getFormElement() { return undefined; } @@ -57,7 +166,7 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '' .min=${this.min} .max=${this.max} .step=${this.step} - .value=${this.valueLow.toString()} + .value=${undefinedFallbackToString(this.valueLow, this.min).toString()} @change=${this.#onChange} ?readonly=${this.readonly}> @@ -71,7 +180,10 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '' .min=${this.min} .max=${this.max} .step=${this.step} - .value="${this.valueLow},${this.valueHigh}" + .value="${undefinedFallbackToString(this.valueLow, this.min).toString()},${undefinedFallbackToString( + this.valueHigh, + this.max, + ).toString()}" @change=${this.#onChange} ?readonly=${this.readonly}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts index 27feebb25cb5..8e3de47b85ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts @@ -1,4 +1,4 @@ -import type { UmbSliderPropertyEditorUiValue } from './types.js'; +import type { UmbSliderPropertyEditorUiValue, UmbSliderPropertyEditorUiValueObject } from './types.js'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import type { UmbInputSliderElement } from '@umbraco-cms/backoffice/components'; import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; @@ -8,15 +8,37 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +function stringToValueObject(value: string | undefined): Partial { + const [from, to] = (value ?? ',').split(','); + const fromNumber = makeNumberOrUndefined(from); + return { from: fromNumber, to: makeNumberOrUndefined(to, fromNumber) }; +} + +function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | number) { + if (value === undefined) { + return fallback; + } + const n = Number(value); + if (isNaN(n)) { + return fallback; + } + return n; +} + +function undefinedFallback(value: number | undefined, fallback: number) { + return value === undefined ? fallback : value; +} /** * @element umb-property-editor-ui-slider */ @customElement('umb-property-editor-ui-slider') -export class UmbPropertyEditorUISliderElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property({ type: Object }) - value: UmbSliderPropertyEditorUiValue | undefined; - +export class UmbPropertyEditorUISliderElement + extends UmbFormControlMixin(UmbLitElement) + implements UmbPropertyEditorUiElement +{ /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. * @type {boolean} @@ -82,6 +104,7 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U } protected override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-slider')!); if (this._min && this._max && this._min > this._max) { console.warn( `Property '${this._label}' (Slider) has been misconfigured, 'min' is greater than 'max'. Please correct your data type configuration.`, @@ -95,13 +118,13 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U return Number.isNaN(num) ? undefined : num; } - #getValueObject(value: string) { - const [from, to] = value.split(',').map(Number); - return { from, to: to ?? from }; - } - #onChange(event: CustomEvent & { target: UmbInputSliderElement }) { - this.value = this.#getValueObject(event.target.value as string); + const partialValue = stringToValueObject(event.target.value as string); + const handledFrom = undefinedFallback(partialValue.from, this._initVal1); + this.value = { + from: handledFrom, + to: this._enableRange ? undefinedFallback(partialValue.to, this._initVal2) : handledFrom, + }; this.dispatchEvent(new UmbChangeEvent()); } @@ -109,8 +132,8 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U return html`