Skip to content

V15: Improve the dropzone for Image Cropper #18838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 27, 2025
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
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,
UmbImageCropperCrops,
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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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 ?? '';
}

constructor() {
super();

this.consumeContext(UMB_APP_CONTEXT, (context) => {
this._serverUrl = context.getServerUrl();
});
}

override updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties);

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;
}
override disconnectedCallback(): void {
super.disconnectedCallback();
if (this.fileDataUrl) {
URL.revokeObjectURL(this.fileDataUrl);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down
Original file line number Diff line number Diff line change
@@ -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 { 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 { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
import { assignToFrozenObject } from '@umbraco-cms/backoffice/observable-api';
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: [],
Expand All @@ -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}
Expand All @@ -45,18 +47,15 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
crops: UmbImageCropperPropertyEditorValue['crops'] = [];

@state()
file?: File;

@state()
fileUnique?: string;
private _file?: UmbUploadableItem;

@state()
private _accept?: string;

@state()
private _loading = true;

#manager = new UmbTemporaryFileManager(this);
#config = new UmbTemporaryFileConfigRepository(this);

constructor() {
super();
Expand All @@ -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;
Expand All @@ -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.dispatchEvent(new UmbChangeEvent());
}
this.value = assignToFrozenObject(this.value ?? DefaultValue, {
temporaryFileId: file.temporaryFile?.temporaryUnique,
});

#onBrowse(e: Event) {
if (!this._dropzone) return;
e.stopImmediatePropagation();
this._dropzone.browse();
this.dispatchEvent(new UmbChangeEvent());
}

#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());
};
Expand Down Expand Up @@ -144,7 +136,7 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
return html`<div id="loader"><uui-loader></uui-loader></div>`;
}

if (this.value?.src || this.file) {
if (this.value?.src || this._file) {
return this.#renderImageCropper();
}

Expand All @@ -153,14 +145,11 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<

#renderDropzone() {
return html`
<uui-file-dropzone
<umb-input-dropzone
id="dropzone"
label="dropzone"
accept=${ifDefined(this._accept)}
@change="${this.#onUpload}"
@click=${this.#onBrowse}>
<uui-button label=${this.localize.term('media_clickToUpload')} @click="${this.#onBrowse}"></uui-button>
</uui-file-dropzone>
disable-folder-upload
@change="${this.#onUpload}"></umb-input-dropzone>
`;
}

Expand All @@ -184,31 +173,24 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
}

#renderImageCropper() {
return html`<umb-image-cropper-field .value=${this.value} .file=${this.file as File} @change=${this.#onChange}>
return html`<umb-image-cropper-field
.value=${this.value}
.file=${this._file?.temporaryFile?.file}
@change=${this.#onChange}>
<uui-button slot="actions" @click=${this.#onRemove} label=${this.localize.term('content_uploadClear')}>
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
</uui-button>
</umb-image-cropper-field> `;
}

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);
}
`,
];
}
Expand Down
Loading