Skip to content

Commit a71ebe1

Browse files
authored
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 bc1a6ae. * 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
1 parent 94f0add commit a71ebe1

File tree

3 files changed

+81
-82
lines changed

3 files changed

+81
-82
lines changed

src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1+
import type { UmbImageCropChangeEvent } from './crop-change.event.js';
2+
import type { UmbFocalPointChangeEvent } from './focalpoint-change.event.js';
13
import type { UmbImageCropperElement } from './image-cropper.element.js';
24
import type {
35
UmbImageCropperCrop,
46
UmbImageCropperCrops,
57
UmbImageCropperFocalPoint,
68
UmbImageCropperPropertyEditorValue,
79
} from './types.js';
8-
import type { UmbImageCropChangeEvent } from './crop-change.event.js';
9-
import type { UmbFocalPointChangeEvent } from './focalpoint-change.event.js';
1010
import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
1111
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
1212
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
13+
import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
1314

14-
import './image-cropper.element.js';
1515
import './image-cropper-focus-setter.element.js';
1616
import './image-cropper-preview.element.js';
17+
import './image-cropper.element.js';
1718

1819
@customElement('umb-image-cropper-field')
1920
export class UmbInputImageCropperFieldElement extends UmbLitElement {
@@ -46,7 +47,19 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
4647
currentCrop?: UmbImageCropperCrop;
4748

4849
@property({ attribute: false })
49-
file?: File;
50+
set file(file: File | undefined) {
51+
this.#file = file;
52+
if (file) {
53+
this.fileDataUrl = URL.createObjectURL(file);
54+
} else if (this.fileDataUrl) {
55+
URL.revokeObjectURL(this.fileDataUrl);
56+
this.fileDataUrl = undefined;
57+
}
58+
}
59+
get file() {
60+
return this.#file;
61+
}
62+
#file?: File;
5063

5164
@property()
5265
fileDataUrl?: string;
@@ -60,25 +73,29 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
6073
@state()
6174
src = '';
6275

63-
get source() {
64-
if (this.fileDataUrl) return this.fileDataUrl;
65-
if (this.src) return this.src;
66-
return '';
76+
@state()
77+
private _serverUrl = '';
78+
79+
get source(): string {
80+
if (this.src) {
81+
return `${this._serverUrl}${this.src}`;
82+
}
83+
84+
return this.fileDataUrl ?? '';
85+
}
86+
87+
constructor() {
88+
super();
89+
90+
this.consumeContext(UMB_APP_CONTEXT, (context) => {
91+
this._serverUrl = context.getServerUrl();
92+
});
6793
}
6894

69-
override updated(changedProperties: Map<string | number | symbol, unknown>) {
70-
super.updated(changedProperties);
71-
72-
if (changedProperties.has('file')) {
73-
if (this.file) {
74-
const reader = new FileReader();
75-
reader.onload = (event) => {
76-
this.fileDataUrl = event.target?.result as string;
77-
};
78-
reader.readAsDataURL(this.file);
79-
} else {
80-
this.fileDataUrl = undefined;
81-
}
95+
override disconnectedCallback(): void {
96+
super.disconnectedCallback();
97+
if (this.fileDataUrl) {
98+
URL.revokeObjectURL(this.fileDataUrl);
8299
}
83100
}
84101

src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ export class UmbImageCropperPreviewElement extends UmbLitElement {
1818
label?: string;
1919

2020
@property({ attribute: false })
21-
get focalPoint() {
22-
return this.#focalPoint;
23-
}
2421
set focalPoint(value) {
2522
this.#focalPoint = value;
2623
this.#onFocalPointUpdated();
2724
}
25+
get focalPoint() {
26+
return this.#focalPoint;
27+
}
2828

2929
#focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 };
3030

src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts

Lines changed: 40 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import type { UmbImageCropperPropertyEditorValue } from './types.js';
22
import type { UmbInputImageCropperFieldElement } from './image-cropper-field.element.js';
3-
import { html, customElement, property, query, state, css, ifDefined } from '@umbraco-cms/backoffice/external/lit';
4-
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
5-
import { UmbId } from '@umbraco-cms/backoffice/id';
3+
import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit';
4+
import { assignToFrozenObject } from '@umbraco-cms/backoffice/observable-api';
65
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
6+
import { UmbFileDropzoneItemStatus, UmbInputDropzoneDashedStyles } from '@umbraco-cms/backoffice/dropzone';
77
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
8-
import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
9-
import { assignToFrozenObject } from '@umbraco-cms/backoffice/observable-api';
8+
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
9+
import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file';
1010
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
11+
import type {
12+
UmbDropzoneChangeEvent,
13+
UmbInputDropzoneElement,
14+
UmbUploadableItem,
15+
} from '@umbraco-cms/backoffice/dropzone';
1116

12-
import './image-cropper.element.js';
17+
import './image-cropper-field.element.js';
1318
import './image-cropper-focus-setter.element.js';
1419
import './image-cropper-preview.element.js';
15-
import './image-cropper-field.element.js';
20+
import './image-cropper.element.js';
1621

1722
const DefaultFocalPoint = { left: 0.5, top: 0.5 };
18-
const DefaultValue = {
23+
const DefaultValue: UmbImageCropperPropertyEditorValue = {
1924
temporaryFileId: null,
2025
src: '',
2126
crops: [],
@@ -28,9 +33,6 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
2833
typeof UmbLitElement,
2934
undefined
3035
>(UmbLitElement, undefined) {
31-
@query('#dropzone')
32-
private _dropzone?: UUIFileDropzoneElement;
33-
3436
/**
3537
* Sets the input to required, meaning validation will fail if the value is empty.
3638
* @type {boolean}
@@ -45,18 +47,15 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
4547
crops: UmbImageCropperPropertyEditorValue['crops'] = [];
4648

4749
@state()
48-
file?: File;
49-
50-
@state()
51-
fileUnique?: string;
50+
private _file?: UmbUploadableItem;
5251

5352
@state()
5453
private _accept?: string;
5554

5655
@state()
5756
private _loading = true;
5857

59-
#manager = new UmbTemporaryFileManager(this);
58+
#config = new UmbTemporaryFileConfigRepository(this);
6059

6160
constructor() {
6261
super();
@@ -76,9 +75,9 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
7675
}
7776

7877
async #observeAcceptedFileTypes() {
79-
const config = await this.#manager.getConfiguration();
78+
await this.#config.initialized;
8079
this.observe(
81-
config.part('imageFileTypes'),
80+
this.#config.part('imageFileTypes'),
8281
(imageFileTypes) => {
8382
this._accept = imageFileTypes.join(',');
8483
this._loading = false;
@@ -87,34 +86,27 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
8786
);
8887
}
8988

90-
#onUpload(e: UUIFileDropzoneEvent) {
91-
const file = e.detail.files[0];
92-
if (!file) return;
93-
const unique = UmbId.new();
89+
#onUpload(e: UmbDropzoneChangeEvent) {
90+
e.stopImmediatePropagation();
9491

95-
this.file = file;
96-
this.fileUnique = unique;
92+
const target = e.target as UmbInputDropzoneElement;
93+
const file = target.value?.[0];
9794

98-
this.value = assignToFrozenObject(this.value ?? DefaultValue, { temporaryFileId: unique });
95+
if (file?.status !== UmbFileDropzoneItemStatus.COMPLETE) return;
9996

100-
this.#manager?.uploadOne({ temporaryUnique: unique, file });
97+
this._file = file;
10198

102-
this.dispatchEvent(new UmbChangeEvent());
103-
}
99+
this.value = assignToFrozenObject(this.value ?? DefaultValue, {
100+
temporaryFileId: file.temporaryFile?.temporaryUnique,
101+
});
104102

105-
#onBrowse(e: Event) {
106-
if (!this._dropzone) return;
107-
e.stopImmediatePropagation();
108-
this._dropzone.browse();
103+
this.dispatchEvent(new UmbChangeEvent());
109104
}
110105

111106
#onRemove = () => {
112107
this.value = undefined;
113-
if (this.fileUnique) {
114-
this.#manager?.removeOne(this.fileUnique);
115-
}
116-
this.fileUnique = undefined;
117-
this.file = undefined;
108+
this._file?.temporaryFile?.abortController?.abort();
109+
this._file = undefined;
118110

119111
this.dispatchEvent(new UmbChangeEvent());
120112
};
@@ -144,7 +136,7 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
144136
return html`<div id="loader"><uui-loader></uui-loader></div>`;
145137
}
146138

147-
if (this.value?.src || this.file) {
139+
if (this.value?.src || this._file) {
148140
return this.#renderImageCropper();
149141
}
150142

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

154146
#renderDropzone() {
155147
return html`
156-
<uui-file-dropzone
148+
<umb-input-dropzone
157149
id="dropzone"
158-
label="dropzone"
159150
accept=${ifDefined(this._accept)}
160-
@change="${this.#onUpload}"
161-
@click=${this.#onBrowse}>
162-
<uui-button label=${this.localize.term('media_clickToUpload')} @click="${this.#onBrowse}"></uui-button>
163-
</uui-file-dropzone>
151+
disable-folder-upload
152+
@change="${this.#onUpload}"></umb-input-dropzone>
164153
`;
165154
}
166155

@@ -184,31 +173,24 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
184173
}
185174

186175
#renderImageCropper() {
187-
return html`<umb-image-cropper-field .value=${this.value} .file=${this.file as File} @change=${this.#onChange}>
176+
return html`<umb-image-cropper-field
177+
.value=${this.value}
178+
.file=${this._file?.temporaryFile?.file}
179+
@change=${this.#onChange}>
188180
<uui-button slot="actions" @click=${this.#onRemove} label=${this.localize.term('content_uploadClear')}>
189181
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
190182
</uui-button>
191183
</umb-image-cropper-field> `;
192184
}
193185

194-
static override styles = [
186+
static override readonly styles = [
187+
UmbTextStyles,
188+
UmbInputDropzoneDashedStyles,
195189
css`
196190
#loader {
197191
display: flex;
198192
justify-content: center;
199193
}
200-
201-
uui-file-dropzone {
202-
position: relative;
203-
display: block;
204-
}
205-
uui-file-dropzone::after {
206-
content: '';
207-
position: absolute;
208-
inset: 0;
209-
cursor: pointer;
210-
border: 1px dashed var(--uui-color-divider-emphasis);
211-
}
212194
`,
213195
];
214196
}

0 commit comments

Comments
 (0)