diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts index beafa144fc..19823f1b47 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts @@ -12,6 +12,7 @@ import './image-cropper.element.js'; import './image-cropper-focus-setter.element.js'; import './image-cropper-preview.element.js'; import './image-cropper-field.element.js'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; const DefaultFocalPoint = { left: 0.5, top: 0.5 }; const DefaultValue = { @@ -22,12 +23,23 @@ const DefaultValue = { }; @customElement('umb-input-image-cropper') -export class UmbInputImageCropperElement extends UmbLitElement { +export class UmbInputImageCropperElement extends UmbFormControlMixin< + UmbImageCropperPropertyEditorValue, + typeof UmbLitElement, + undefined +>(UmbLitElement, undefined) { @query('#dropzone') private _dropzone?: UUIFileDropzoneElement; - @property({ attribute: false }) - value: UmbImageCropperPropertyEditorValue = DefaultValue; + /** + * 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; @property({ attribute: false }) crops: UmbImageCropperPropertyEditorValue['crops'] = []; @@ -43,6 +55,14 @@ export class UmbInputImageCropperElement extends UmbLitElement { constructor() { super(); this.#manager = new UmbTemporaryFileManager(this); + + this.addValidator( + 'valueMissing', + () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => { + return !!this.required && (!this.value || (this.value.src === '' && this.value.temporaryFileId == null)); + }, + ); } protected override firstUpdated(): void { @@ -57,7 +77,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.file = file; this.fileUnique = unique; - this.value = assignToFrozenObject(this.value, { temporaryFileId: unique }); + this.value = assignToFrozenObject(this.value ?? DefaultValue, { temporaryFileId: unique }); this.#manager?.uploadOne({ temporaryUnique: unique, file }); @@ -71,7 +91,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { } #onRemove = () => { - this.value = assignToFrozenObject(this.value, DefaultValue); + this.value = undefined; if (this.fileUnique) { this.#manager?.removeOne(this.fileUnique); } @@ -82,25 +102,27 @@ export class UmbInputImageCropperElement extends UmbLitElement { }; #mergeCrops() { - // Replace crops from the value with the crops from the config while keeping the coordinates from the value if they exist. - const filteredCrops = this.crops.map((crop) => { - const cropFromValue = this.value.crops.find((valueCrop) => valueCrop.alias === crop.alias); - const result = { - ...crop, - coordinates: cropFromValue?.coordinates ?? undefined, + if (this.value) { + // Replace crops from the value with the crops from the config while keeping the coordinates from the value if they exist. + const filteredCrops = this.crops.map((crop) => { + const cropFromValue = this.value!.crops.find((valueCrop) => valueCrop.alias === crop.alias); + const result = { + ...crop, + coordinates: cropFromValue?.coordinates ?? undefined, + }; + + return result; + }); + + this.value = { + ...this.value, + crops: filteredCrops, }; - - return result; - }); - - this.value = { - ...this.value, - crops: filteredCrops, - }; + } } override render() { - if (this.value.src || this.file) { + if (this.value?.src || this.file) { return this.#renderImageCropper(); } @@ -119,7 +141,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { const value = (e.target as UmbInputImageCropperFieldElement).value; if (!value) { - this.value = DefaultValue; + this.value = undefined; this.dispatchEvent(new UmbChangeEvent()); return; } @@ -128,7 +150,9 @@ export class UmbInputImageCropperElement extends UmbLitElement { value.temporaryFileId = this.value.temporaryFileId; } - this.value = value; + if (value.temporaryFileId || value.src !== '') { + this.value = value; + } this.dispatchEvent(new UmbChangeEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts index b7f7ea2acc..dc2eed16e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts @@ -1,59 +1,62 @@ import type { UmbImageCropperPropertyEditorValue, UmbInputImageCropperElement } from '../../components/index.js'; -import { html, customElement, property, nothing, state } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { type UmbPropertyEditorUiElement, UmbPropertyValueChangeEvent, type UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + import '../../components/input-image-cropper/input-image-cropper.element.js'; /** * @element umb-property-editor-ui-image-cropper */ @customElement('umb-property-editor-ui-image-cropper') -export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property({ attribute: false }) - value: UmbImageCropperPropertyEditorValue = { - temporaryFileId: null, - src: '', - crops: [], - focalPoint: { left: 0.5, top: 0.5 }, - }; +export class UmbPropertyEditorUIImageCropperElement + extends UmbFormControlMixin( + UmbLitElement, + ) + implements UmbPropertyEditorUiElement +{ + /** + * Sets the input to mandatory, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + mandatory?: boolean; + + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; @state() crops: UmbImageCropperPropertyEditorValue['crops'] = []; - override updated(changedProperties: Map) { - super.updated(changedProperties); - if (changedProperties.has('value')) { - if (!this.value) { - this.value = { - temporaryFileId: null, - src: '', - crops: [], - focalPoint: { left: 0.5, top: 0.5 }, - }; - } - } - } - public set config(config: UmbPropertyEditorConfigCollection | undefined) { this.crops = config?.getValueByAlias('crops') ?? []; } + override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-image-cropper')!); + } + + override focus() { + return this.shadowRoot?.querySelector('umb-input-image-cropper')?.focus(); + } + #onChange(e: Event) { this.value = (e.target as UmbInputImageCropperElement).value; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } override render() { - if (!this.value) return nothing; - return html``; + .crops=${this.crops} + .required=${this.mandatory} + .requiredMessage=${this.mandatoryMessage}>`; } }