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
This commit is contained in:
@@ -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<string | number | symbol, unknown>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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`<div id="loader"><uui-loader></uui-loader></div>`;
|
||||
}
|
||||
|
||||
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`
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user