Skip to content
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