From a71ebe1902c78dbf7cbefdca041043dd8d84f215 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:11:37 +0100 Subject: [PATCH] V15: Improve the dropzone for Image Cropper (#18838) * feat: uses the umb-dropzone-input to render the dropzone * feat: loads in the blob url rather than reading the file into memory AND appends the server url * chore: lit 3 compat * feat: uses the umb-dropzone-input to render the dropzone * Revert "feat: uses the umb-dropzone-input to render the dropzone" This reverts commit bc1a6ae7df2e3230a132ce1a3756c7b2348647f9. * feat: creates an object url directly from the File rather than the Blob * feat: revokes the file data url from object storage * feat: revokes object url on disconnect --- .../image-cropper-field.element.ts | 57 ++++++---- .../image-cropper-preview.element.ts | 6 +- .../input-image-cropper.element.ts | 100 +++++++----------- 3 files changed, 81 insertions(+), 82 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts index 5ba36c22b8..b8d403ba09 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts @@ -1,3 +1,5 @@ +import type { UmbImageCropChangeEvent } from './crop-change.event.js'; +import type { UmbFocalPointChangeEvent } from './focalpoint-change.event.js'; import type { UmbImageCropperElement } from './image-cropper.element.js'; import type { UmbImageCropperCrop, @@ -5,15 +7,14 @@ import type { UmbImageCropperFocalPoint, UmbImageCropperPropertyEditorValue, } from './types.js'; -import type { UmbImageCropChangeEvent } from './crop-change.event.js'; -import type { UmbFocalPointChangeEvent } from './focalpoint-change.event.js'; import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; -import './image-cropper.element.js'; import './image-cropper-focus-setter.element.js'; import './image-cropper-preview.element.js'; +import './image-cropper.element.js'; @customElement('umb-image-cropper-field') export class UmbInputImageCropperFieldElement extends UmbLitElement { @@ -46,7 +47,19 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement { currentCrop?: UmbImageCropperCrop; @property({ attribute: false }) - file?: File; + set file(file: File | undefined) { + this.#file = file; + if (file) { + this.fileDataUrl = URL.createObjectURL(file); + } else if (this.fileDataUrl) { + URL.revokeObjectURL(this.fileDataUrl); + this.fileDataUrl = undefined; + } + } + get file() { + return this.#file; + } + #file?: File; @property() fileDataUrl?: string; @@ -60,25 +73,29 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement { @state() src = ''; - get source() { - if (this.fileDataUrl) return this.fileDataUrl; - if (this.src) return this.src; - return ''; + @state() + private _serverUrl = ''; + + get source(): string { + if (this.src) { + return `${this._serverUrl}${this.src}`; + } + + return this.fileDataUrl ?? ''; } - override updated(changedProperties: Map) { - super.updated(changedProperties); + constructor() { + super(); - if (changedProperties.has('file')) { - if (this.file) { - const reader = new FileReader(); - reader.onload = (event) => { - this.fileDataUrl = event.target?.result as string; - }; - reader.readAsDataURL(this.file); - } else { - this.fileDataUrl = undefined; - } + this.consumeContext(UMB_APP_CONTEXT, (context) => { + this._serverUrl = context.getServerUrl(); + }); + } + + override disconnectedCallback(): void { + super.disconnectedCallback(); + if (this.fileDataUrl) { + URL.revokeObjectURL(this.fileDataUrl); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts index a092f0868c..d203a1af7e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts @@ -18,13 +18,13 @@ export class UmbImageCropperPreviewElement extends UmbLitElement { label?: string; @property({ attribute: false }) - get focalPoint() { - return this.#focalPoint; - } set focalPoint(value) { this.#focalPoint = value; this.#onFocalPointUpdated(); } + get focalPoint() { + return this.#focalPoint; + } #focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 }; 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 b90bd8dd20..1bf0a945c4 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 @@ -1,21 +1,26 @@ import type { UmbImageCropperPropertyEditorValue } from './types.js'; import type { UmbInputImageCropperFieldElement } from './image-cropper-field.element.js'; -import { html, customElement, property, query, state, css, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbId } from '@umbraco-cms/backoffice/id'; -import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; import { assignToFrozenObject } from '@umbraco-cms/backoffice/observable-api'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFileDropzoneItemStatus, UmbInputDropzoneDashedStyles } from '@umbraco-cms/backoffice/dropzone'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file'; import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { + UmbDropzoneChangeEvent, + UmbInputDropzoneElement, + UmbUploadableItem, +} from '@umbraco-cms/backoffice/dropzone'; -import './image-cropper.element.js'; +import './image-cropper-field.element.js'; import './image-cropper-focus-setter.element.js'; import './image-cropper-preview.element.js'; -import './image-cropper-field.element.js'; +import './image-cropper.element.js'; const DefaultFocalPoint = { left: 0.5, top: 0.5 }; -const DefaultValue = { +const DefaultValue: UmbImageCropperPropertyEditorValue = { temporaryFileId: null, src: '', crops: [], @@ -28,9 +33,6 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< typeof UmbLitElement, undefined >(UmbLitElement, undefined) { - @query('#dropzone') - private _dropzone?: UUIFileDropzoneElement; - /** * Sets the input to required, meaning validation will fail if the value is empty. * @type {boolean} @@ -45,10 +47,7 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< crops: UmbImageCropperPropertyEditorValue['crops'] = []; @state() - file?: File; - - @state() - fileUnique?: string; + private _file?: UmbUploadableItem; @state() private _accept?: string; @@ -56,7 +55,7 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< @state() private _loading = true; - #manager = new UmbTemporaryFileManager(this); + #config = new UmbTemporaryFileConfigRepository(this); constructor() { super(); @@ -76,9 +75,9 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< } async #observeAcceptedFileTypes() { - const config = await this.#manager.getConfiguration(); + await this.#config.initialized; this.observe( - config.part('imageFileTypes'), + this.#config.part('imageFileTypes'), (imageFileTypes) => { this._accept = imageFileTypes.join(','); this._loading = false; @@ -87,34 +86,27 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< ); } - #onUpload(e: UUIFileDropzoneEvent) { - const file = e.detail.files[0]; - if (!file) return; - const unique = UmbId.new(); + #onUpload(e: UmbDropzoneChangeEvent) { + e.stopImmediatePropagation(); - this.file = file; - this.fileUnique = unique; + const target = e.target as UmbInputDropzoneElement; + const file = target.value?.[0]; - this.value = assignToFrozenObject(this.value ?? DefaultValue, { temporaryFileId: unique }); + if (file?.status !== UmbFileDropzoneItemStatus.COMPLETE) return; - this.#manager?.uploadOne({ temporaryUnique: unique, file }); + this._file = file; + + this.value = assignToFrozenObject(this.value ?? DefaultValue, { + temporaryFileId: file.temporaryFile?.temporaryUnique, + }); this.dispatchEvent(new UmbChangeEvent()); } - #onBrowse(e: Event) { - if (!this._dropzone) return; - e.stopImmediatePropagation(); - this._dropzone.browse(); - } - #onRemove = () => { this.value = undefined; - if (this.fileUnique) { - this.#manager?.removeOne(this.fileUnique); - } - this.fileUnique = undefined; - this.file = undefined; + this._file?.temporaryFile?.abortController?.abort(); + this._file = undefined; this.dispatchEvent(new UmbChangeEvent()); }; @@ -144,7 +136,7 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< return html`
`; } - if (this.value?.src || this.file) { + if (this.value?.src || this._file) { return this.#renderImageCropper(); } @@ -153,14 +145,11 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< #renderDropzone() { return html` - - - + disable-folder-upload + @change="${this.#onUpload}"> `; } @@ -184,31 +173,24 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< } #renderImageCropper() { - return html` + return html` ${this.localize.term('content_uploadClear')} `; } - static override styles = [ + static override readonly styles = [ + UmbTextStyles, + UmbInputDropzoneDashedStyles, css` #loader { display: flex; justify-content: center; } - - uui-file-dropzone { - position: relative; - display: block; - } - uui-file-dropzone::after { - content: ''; - position: absolute; - inset: 0; - cursor: pointer; - border: 1px dashed var(--uui-color-divider-emphasis); - } `, ]; }