Media Picker Modal File Previews (#19335)

* Ordered `@property` setters/getters

* JSDocs + comments

* Removed unneeded CSS

* Markup code formatting

* Hides "Reset focal point" button

when focal point has default value

* Image Cropper field element: adds active state to crops

* Image Cropper Editor Field element: reduced the markup and styles

Removing duplications from inherited class

* Removed unused code from Image Cropper Focus Setter element

* Big refactor of Image Cropper Editor modal

to support File Upload Previews

* Added `<umb-file-upload-preview>`

to handle the logic of rendering the relevant `fileUploadPreview` extension.

* Refactored SVG File Upload Preview component

Removes the `<uui-card-media>` container.
Controversially removes the link, but this was inconsistent with other file previews.

* Refactored General File Upload Preview component

Removes the `<uui-card-media>` container.
Controversially removes the link, but this was inconsistent with other file previews.

* Refactored Image File Upload Preview component

Removes the `<uui-card-media>` container.
Controversially removes the link, but this was inconsistent with other file previews.

* Refactored Audio and Video File Upload Preview component

To align code with the other file previews.

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/modals/image-cropper-editor/image-cropper-editor-modal.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
Lee Kelleher
2025-05-16 11:29:09 +01:00
committed by GitHub
parent 76fcb4ade2
commit e62e55d1c0
18 changed files with 407 additions and 308 deletions

View File

@@ -39,14 +39,14 @@ export class UmbExtensionSlotElement extends UmbLitElement {
* <umb-extension-slot .type=${['my-extension-type','another-extension-type']}></umb-extension-slot>
*/
@property({ type: String })
public get type(): string | string[] | undefined {
return this.#type;
}
public set type(value: string | string[] | undefined) {
if (value === this.#type) return;
this.#type = value;
this.#observeExtensions();
}
public get type(): string | string[] | undefined {
return this.#type;
}
#type?: string | string[] | undefined;
/**
@@ -58,14 +58,14 @@ export class UmbExtensionSlotElement extends UmbLitElement {
* <umb-extension-slot type="my-extension-type" .filter=${(ext) => ext.meta.anyPropToFilter === 'foo'}></umb-extension-slot>
*/
@property({ type: Object, attribute: false })
public get filter(): (manifest: any) => boolean {
return this.#filter;
}
public set filter(value: (manifest: any) => boolean) {
if (value === this.#filter) return;
this.#filter = value;
this.#observeExtensions();
}
public get filter(): (manifest: any) => boolean {
return this.#filter;
}
#filter: (manifest: any) => boolean = () => true;
/**
@@ -77,15 +77,15 @@ export class UmbExtensionSlotElement extends UmbLitElement {
* <umb-extension-slot type="my-extension-type" .props=${{foo: 'bar'}}></umb-extension-slot>
*/
@property({ type: Object, attribute: false })
get props(): Record<string, unknown> | undefined {
return this.#props;
}
set props(newVal: Record<string, unknown> | undefined) {
this.#props = newVal;
if (this.#extensionsController) {
this.#extensionsController.properties = newVal;
}
}
get props(): Record<string, unknown> | undefined {
return this.#props;
}
#props?: Record<string, unknown> = {};
@property({ type: String, attribute: 'default-element' })

View File

@@ -44,6 +44,7 @@ export class UmbTemporaryFileConfigRepository extends UmbRepositoryBase implemen
/**
* Subscribe to the entire configuration.
* @returns {Observable<UmbTemporaryFileConfigurationModel>}
*/
all() {
if (!this.#dataStore) {
@@ -56,6 +57,7 @@ export class UmbTemporaryFileConfigRepository extends UmbRepositoryBase implemen
/**
* Subscribe to a part of the configuration.
* @param part
* @returns {Observable<UmbTemporaryFileConfigurationModel[Part]>}
*/
part<Part extends keyof UmbTemporaryFileConfigurationModel>(
part: Part,

View File

@@ -99,14 +99,8 @@ export class UmbImagingThumbnailElement extends UmbLitElement {
return when(
this._thumbnailUrl,
() =>
html`<img
id="figure"
src="${this._thumbnailUrl}"
alt="${this.alt}"
loading="${this.loading}"
draggable="false" />`,
() => html`<umb-icon id="icon" name="${this.icon}"></umb-icon>`,
(url) => html`<img id="figure" src=${url} alt=${this.alt} loading=${this.loading} draggable="false" />`,
() => html`<umb-icon id="icon" name=${this.icon}></umb-icon>`,
);
}

View File

@@ -11,7 +11,7 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
> {
/**
* The thumbnail items that are currently displayed in the collection.
* @deprecated Use the `<umb-imaging-thumbnail>` element instead.
* @deprecated Use the `<umb-imaging-thumbnail>` element instead. This will be removed in Umbraco 17.
*/
public readonly thumbnailItems = this.items;

View File

@@ -175,69 +175,91 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
}
protected renderActions() {
return html`<slot name="actions"></slot>
return html`
<slot name="actions"></slot>
${when(
!this.hideFocalPoint,
() =>
html`<uui-button
label=${this.localize.term('content_resetFocalPoint')}
@click=${this.onResetFocalPoint}></uui-button>`,
)} `;
!this.hideFocalPoint && this.focalPoint.left !== 0.5 && this.focalPoint.top !== 0.5,
() => html`
<uui-button compact label=${this.localize.term('content_resetFocalPoint')} @click=${this.onResetFocalPoint}>
<uui-icon name="icon-axis-rotation"></uui-icon>
${this.localize.term('content_resetFocalPoint')}
</uui-button>
`,
)}
`;
}
protected renderSide() {
if (!this.value || !this.crops) return;
return repeat(
this.crops,
(crop) => crop.alias + JSON.stringify(crop.coordinates),
(crop) =>
html` <umb-image-cropper-preview
@click=${() => this.onCropClick(crop)}
(crop) => html`
<umb-image-cropper-preview
.crop=${crop}
.focalPoint=${this.focalPoint}
.src=${this.source}></umb-image-cropper-preview>`,
.src=${this.source}
?active=${this.currentCrop?.alias === crop.alias}
@click=${() => this.onCropClick(crop)}>
</umb-image-cropper-preview>
`,
);
}
static override styles = css`
:host {
display: flex;
width: 100%;
box-sizing: border-box;
gap: var(--uui-size-space-3);
height: 400px;
}
#main {
max-width: 500px;
min-width: 300px;
width: 100%;
height: 100%;
display: flex;
gap: var(--uui-size-space-1);
flex-direction: column;
}
static override styles = [
css`
:host {
display: flex;
width: 100%;
box-sizing: border-box;
gap: var(--uui-size-space-3);
height: 400px;
}
#actions {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
}
#main {
max-width: 500px;
min-width: 300px;
width: 100%;
height: 100%;
display: flex;
gap: var(--uui-size-space-1);
flex-direction: column;
}
umb-image-cropper-focus-setter {
height: calc(100% - 33px - var(--uui-size-space-1)); /* Temp solution to make room for actions */
}
#actions {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
#side {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: var(--uui-size-space-3);
flex-grow: 1;
overflow-y: auto;
height: fit-content;
max-height: 100%;
}
`;
uui-icon {
padding-right: var(--uui-size-1);
}
}
slot[name='actions'] {
display: block;
flex: 1;
}
umb-image-cropper-focus-setter {
height: calc(100% - 33px - var(--uui-size-space-1)); /* Temp solution to make room for actions */
}
#side {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: var(--uui-size-space-3);
flex-grow: 1;
overflow-y: auto;
height: fit-content;
max-height: 100%;
umb-image-cropper-preview[active] {
background-color: var(--uui-color-current);
}
}
`,
];
}
declare global {

View File

@@ -107,19 +107,10 @@ export class UmbImageCropperFocusSetterElement extends UmbLitElement {
}
}
#coordsToFactor(x: number, y: number) {
const top = (y / 100 / y) * 50;
const left = (x / 100 / x) * 50;
return { top, left };
}
#setFocalPoint(x: number, y: number, width: number, height: number) {
const left = clamp(x / width, 0, 1);
const top = clamp(y / height, 0, 1);
this.#coordsToFactor(x, y);
const focalPoint = { left, top } as UmbFocalPointModel;
this.dispatchEvent(new UmbFocalPointChangeEvent(focalPoint));
@@ -252,12 +243,9 @@ export class UmbImageCropperFocusSetterElement extends UmbLitElement {
<img id="image" @keydown=${() => nothing} src=${this.src} alt="" />
<span
id="focal-point"
class=${classMap({
'focal-point--dragging': this._isDraggingGridHandle,
hidden: this.hideFocalPoint,
})}
class=${classMap({ 'focal-point--dragging': this._isDraggingGridHandle, hidden: this.hideFocalPoint })}
tabindex=${ifDefined(this.disabled ? undefined : '0')}
aria-label="${this.localize.term('general_focalPoint')}"
aria-label=${this.localize.term('general_focalPoint')}
@keydown=${this.#handleGridKeyDown}>
</span>
</div>

View File

@@ -173,14 +173,14 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
}
#renderImageCropper() {
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> `;
return html`
<umb-image-cropper-field .value=${this.value} .file=${this._file?.temporaryFile?.file} @change=${this.#onChange}>
<uui-button slot="actions" compact label=${this.localize.term('content_uploadClear')} @click=${this.#onRemove}>
<uui-icon name="icon-trash"></uui-icon>
<umb-localize key="content_uploadClear">Remove file(s)</umb-localize>
</uui-button>
</umb-image-cropper-field>
`;
}
static override readonly styles = [
@@ -190,10 +190,17 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
max-width: 500px;
min-width: 300px;
}
#loader {
display: flex;
justify-content: center;
}
[slot='actions'] {
uui-icon {
padding-right: var(--uui-size-1);
}
}
`,
];
}

View File

@@ -0,0 +1,135 @@
import { getMimeTypeFromExtension } from '../../components/index.js';
import type { ManifestFileUploadPreview } from './file-upload-preview.extension.js';
import type { UmbFileUploadPreviewElement as UmbFileUploadPreviewElementInterface } from './file-upload-preview.interface.js';
import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-file-upload-preview')
export class UmbFileUploadPreviewElement extends UmbLitElement implements UmbFileUploadPreviewElementInterface {
@state()
private _previewAlias?: string;
@property({ type: Object })
file?: File;
@property({ type: String })
public set path(value) {
this.#path = value;
this.#setPreviewAlias();
}
public get path() {
return this.#path;
}
#path? = '';
#manifests: Array<ManifestFileUploadPreview> = [];
async #setPreviewAlias(): Promise<void> {
if (!this.path) return;
const manifests = await this.#getManifests();
const fallbackAlias = manifests.find((manifest) =>
stringOrStringArrayContains(manifest.forMimeTypes, '*/*'),
)?.alias;
let mimeType: string | null = null;
if (this.file) {
mimeType = this.file.type;
} else {
mimeType = this.#getMimeTypeFromPath(this.path);
}
if (!mimeType) {
this._previewAlias = fallbackAlias;
return;
}
// Check for an exact match
const exactMatch = manifests.find((manifest) => {
return stringOrStringArrayContains(manifest.forMimeTypes, mimeType);
});
if (exactMatch) {
this._previewAlias = exactMatch.alias;
return;
}
// Check for wildcard match (e.g. image/*)
const wildcardMatch = manifests.find((manifest) => {
const forMimeTypes = Array.isArray(manifest.forMimeTypes) ? manifest.forMimeTypes : [manifest.forMimeTypes];
return forMimeTypes.find((type) => {
const snippet = type.replace(/\*/g, '');
if (mimeType.startsWith(snippet)) return manifest.alias;
if (mimeType.endsWith(snippet)) return manifest.alias;
return undefined;
});
});
if (wildcardMatch) {
this._previewAlias = wildcardMatch.alias;
return;
}
// Use fallbackAlias.
this._previewAlias = fallbackAlias;
}
async #getManifests() {
if (this.#manifests.length) return this.#manifests;
await new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'fileUploadPreview', null, (exts) => {
this.#manifests = exts.map((exts) => exts.manifest);
}).asPromise();
return this.#manifests;
}
#getMimeTypeFromPath(path: string) {
// Extract the the MIME type from the data url
if (path.startsWith('data:')) {
const mimeType = path.substring(5, path.indexOf(';'));
return mimeType;
}
// Extract the file extension from the path
const extension = path.split('.').pop()?.toLowerCase();
if (!extension) return null;
return getMimeTypeFromExtension('.' + extension);
}
override render() {
if (!this.path) return nothing;
return html`
<umb-extension-slot
single
type="fileUploadPreview"
.props=${{ path: this.path, file: this.file }}
.filter=${(manifest: ManifestFileUploadPreview) => manifest.alias === this._previewAlias}>
</umb-extension-slot>
`;
}
static override readonly styles = [
css`
:host {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
`,
];
}
export { UmbFileUploadPreviewElement as element };
declare global {
interface HTMLElementTagNameMap {
'umb-file-upload-preview': UmbFileUploadPreviewElement;
}
}

View File

@@ -1,19 +1,16 @@
import type { UmbFileUploadPreviewElement } from '../file-upload-preview.interface.js';
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-input-upload-field-audio')
export default class UmbInputUploadFieldAudioElement extends UmbLitElement {
export default class UmbInputUploadFieldAudioElement extends UmbLitElement implements UmbFileUploadPreviewElement {
@property({ type: String })
path = '';
get #label() {
return this.path.split('/').pop() ?? '';
}
override render() {
if (!this.path) return html`<uui-loader></uui-loader>`;
return html`<audio controls src=${this.path} title=${this.#label}></audio>`;
const label = this.path.split('/').pop() ?? '';
return html`<audio controls src=${this.path} title=${label}></audio>`;
}
static override readonly styles = [
@@ -23,6 +20,7 @@ export default class UmbInputUploadFieldAudioElement extends UmbLitElement {
width: 999px;
max-width: 100%;
}
audio {
width: 100%;
}

View File

@@ -1,39 +1,16 @@
import type { UmbFileUploadPreviewElement } from '../file-upload-preview.interface.js';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-input-upload-field-file')
export default class UmbInputUploadFieldFileElement extends UmbLitElement {
export default class UmbInputUploadFieldFileElement extends UmbLitElement implements UmbFileUploadPreviewElement {
@property()
path: string = '';
/**
* @description The file to be rendered.
* @type {File}
*/
@property({ attribute: false })
file?: File;
get #label() {
if (this.file) {
return this.file.name;
}
return this.path.split('/').pop() ?? `(${this.localize.term('general_loading')}...)`;
}
get #fileExtension() {
return this.#label.split('.').pop() ?? '';
}
override render() {
if (!this.path && !this.file) return html`<uui-loader></uui-loader>`;
return html`
<uui-card-media
.name=${this.#label}
.fileExt=${this.#fileExtension}
href=${this.path}
target="_blank"></uui-card-media>
`;
if (!this.path) return html`<uui-loader></uui-loader>`;
const fileExt = this.path.split('.').pop() ?? '';
return html`<uui-symbol-file .type=${fileExt}></uui-symbol-file>`;
}
}

View File

@@ -1,23 +1,16 @@
import type { UmbFileUploadPreviewElement } from '../file-upload-preview.interface.js';
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-input-upload-field-image')
export default class UmbInputUploadFieldImageElement extends UmbLitElement {
export default class UmbInputUploadFieldImageElement extends UmbLitElement implements UmbFileUploadPreviewElement {
@property({ type: String })
path = '';
get #label() {
return this.path.split('/').pop() ?? '';
}
override render() {
if (!this.path) return html`<uui-loader></uui-loader>`;
const label = this.#label;
return html`<uui-card-media id="card" .name=${label} href="${this.path}" target="_blank">
<img id="image" src=${this.path} alt="${label}" loading="lazy" />
</uui-card-media>`;
const label = this.path.split('/').pop() ?? '';
return html`<img src=${this.path} alt=${label} loading="lazy" />`;
}
static override readonly styles = [
@@ -30,12 +23,7 @@ export default class UmbInputUploadFieldImageElement extends UmbLitElement {
max-width: 100%;
}
#card {
width: 100%;
height: 100%;
}
#image {
img {
object-fit: contain;
width: auto;
height: 100%;

View File

@@ -1,41 +1,28 @@
import type { UmbFileUploadPreviewElement } from '../file-upload-preview.interface.js';
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-input-upload-field-svg')
export default class UmbInputUploadFieldSvgElement extends UmbLitElement {
export default class UmbInputUploadFieldSvgElement extends UmbLitElement implements UmbFileUploadPreviewElement {
@property({ type: String })
path = '';
get #label() {
return this.path.split('/').pop() ?? '';
}
override render() {
if (!this.path) return html`<uui-loader></uui-loader>`;
const label = this.#label;
return html`<uui-card-media id="card" .name=${label} href="${this.path}" target="_blank">
<img id="image" src=${this.path} alt="${label}" loading="lazy" />
</uui-card-media>`;
const label = this.path.split('/').pop() ?? '';
return html`<img src=${this.path} alt=${label} loading="lazy" />`;
}
static override readonly styles = [
css`
:host {
position: relative;
min-height: 240px;
max-height: 400px;
width: fit-content;
max-width: 100%;
}
#card {
width: 100%;
height: 100%;
}
#image {
img {
object-fit: contain;
width: auto;
height: 100%;

View File

@@ -1,20 +1,17 @@
import type { UmbFileUploadPreviewElement } from '../file-upload-preview.interface.js';
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-input-upload-field-video')
export default class UmbInputUploadFieldVideoElement extends UmbLitElement {
export default class UmbInputUploadFieldVideoElement extends UmbLitElement implements UmbFileUploadPreviewElement {
@property({ type: String })
path = '';
get #label() {
return this.path.split('/').pop() ?? '';
}
override render() {
if (!this.path) return html`<uui-loader></uui-loader>`;
const label = this.path.split('/').pop() ?? '';
return html`
<video controls title=${this.#label}>
<video controls title=${label}>
<source src=${this.path} />
Video format not supported
</video>
@@ -23,10 +20,9 @@ export default class UmbInputUploadFieldVideoElement extends UmbLitElement {
static override readonly styles = [
css`
:host {
display: flex;
}
video {
height: 100%;
max-height: 500px;
width: 100%;
max-width: 800px;
}

View File

@@ -10,7 +10,7 @@ export const manifests: Array<UmbExtensionManifest> = [
{
type: 'fileUploadPreview',
alias: 'Umb.FileUploadPreview.File',
name: 'File File Upload Preview',
name: 'General File Upload Preview',
weight: 100,
element: () => import('./input-upload-field-file.element.js'),
forMimeTypes: ['*/*'],
@@ -26,7 +26,7 @@ export const manifests: Array<UmbExtensionManifest> = [
{
type: 'fileUploadPreview',
alias: 'Umb.FileUploadPreview.Svg',
name: 'Svg File Upload Preview',
name: 'SVG File Upload Preview',
weight: 100,
element: () => import('./input-upload-field-svg.element.js'),
forMimeTypes: ['image/svg+xml'],

View File

@@ -1,5 +1,5 @@
import { UmbInputImageCropperFieldElement } from '../../../components/input-image-cropper/image-cropper-field.element.js';
import { css, customElement, html, repeat, when } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
@customElement('umb-image-cropper-editor-field')
export class UmbImageCropperEditorFieldElement extends UmbInputImageCropperFieldElement {
@@ -7,98 +7,31 @@ export class UmbImageCropperEditorFieldElement extends UmbInputImageCropperField
this.currentCrop = undefined;
}
override renderActions() {
override renderSide() {
if (!this.value || !this.crops) return;
return html`
<slot name="actions"></slot>
${when(
!this.hideFocalPoint,
() => html`
<uui-button
compact
id="reset-focal-point"
label=${this.localize.term('content_resetFocalPoint')}
@click=${this.onResetFocalPoint}>
<uui-icon name="icon-axis-rotation"></uui-icon>
${this.localize.term('content_resetFocalPoint')}
</uui-button>
`,
)}
<umb-image-cropper-preview
.label=${this.localize.term('general_media')}
?active=${!this.currentCrop}
@click=${this.#resetCurrentCrop}>
</umb-image-cropper-preview>
${super.renderSide()}
`;
}
override renderSide() {
if (!this.value || !this.crops) return;
static override styles = [
...super.styles,
css`
#main {
max-width: unset;
min-width: unset;
}
return html` <umb-image-cropper-preview
@click=${this.#resetCurrentCrop}
?active=${!this.currentCrop}
.label=${this.localize.term('general_media')}></umb-image-cropper-preview>
${repeat(
this.crops,
(crop) => crop.alias + JSON.stringify(crop.coordinates),
(crop) => html`
<umb-image-cropper-preview
?active=${this.currentCrop?.alias === crop.alias}
@click=${() => this.onCropClick(crop)}
.crop=${crop}
.focalPoint=${this.focalPoint}
.src=${this.source}></umb-image-cropper-preview>
`,
)}`;
}
static override styles = css`
:host {
display: flex;
width: 100%;
box-sizing: border-box;
gap: var(--uui-size-space-3);
height: 400px;
}
#main {
width: 100%;
height: 100%;
display: flex;
gap: var(--uui-size-space-1);
flex-direction: column;
}
#actions {
display: flex;
justify-content: space-between;
}
#reset-focal-point uui-icon {
padding-right: var(--uui-size-3);
}
slot[name='actions'] {
display: block;
flex: 1;
}
#reset-current-crop[active],
[active] {
background-color: var(--uui-color-current);
}
umb-image-cropper-focus-setter {
height: calc(100% - 33px - var(--uui-size-space-1)); /* Temp solution to make room for actions */
}
#side {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
flex: none;
gap: var(--uui-size-space-3);
flex-grow: 1;
overflow-y: auto;
height: fit-content;
max-height: 100%;
}
`;
#side {
flex: none;
}
`,
];
}
declare global {

View File

@@ -1,20 +1,22 @@
import { UMB_MEDIA_PICKER_MODAL } from '../media-picker/media-picker-modal.token.js';
import type { UmbCropModel } from '../../types.js';
import type { UmbInputImageCropperFieldElement } from '../../components/input-image-cropper/image-cropper-field.element.js';
import type { UmbImageCropperPropertyEditorValue } from '../../components/index.js';
import { UmbMediaUrlRepository } from '../../url/index.js';
import type { UmbCropModel, UmbMediaItemModel } from '../../types.js';
import type { UmbImageCropperPropertyEditorValue } from '../../components/index.js';
import type { UmbInputImageCropperFieldElement } from '../../components/input-image-cropper/image-cropper-field.element.js';
import type {
UmbImageCropperEditorModalData,
UmbImageCropperEditorModalValue,
} from './image-cropper-editor-modal.token.js';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_MODAL_MANAGER_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import './components/image-cropper-editor-field.element.js';
/** TODO Make some of the components from property editor image cropper reuseable for this modal... */
import './components/image-cropper-editor-field.element.js';
import '../../components/input-upload-field/file-upload-preview.element.js';
@customElement('umb-image-cropper-editor-modal')
export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
@@ -42,7 +44,14 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
private _editMediaPath = '';
@state()
private _pickableFilter?: (item: any) => boolean;
private _pickableFilter?: (item: UmbMediaItemModel) => boolean;
@state()
private _isCroppable = false;
#config = new UmbTemporaryFileConfigRepository(this);
#imageFileTypes?: Array<string>;
#modalManager?: UmbModalManagerContext;
@@ -72,14 +81,32 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
this._crops = this.data?.cropOptions ?? [];
this._pickableFilter = this.data?.pickableFilter;
this.#observeAcceptedFileTypes();
this.#getSrc();
}
async #observeAcceptedFileTypes() {
await this.#config.initialized;
this.observe(
this.#config.part('imageFileTypes'),
(imageFileTypes) => (this.#imageFileTypes = imageFileTypes),
'_observeFileTypes',
);
}
async #getSrc() {
const { data } = await this.#urlRepository.requestItems([this._unique]);
const item = data?.[0];
if (!item?.url) return;
if (!item?.url) {
this._isCroppable = false;
this._imageCropperValue = undefined;
return;
}
if (item.extension && this.#imageFileTypes?.includes(item.extension)) {
this._isCroppable = true;
}
/**
* Combine the crops from the property editor with the stored crops and ignore any invalid crops
@@ -117,6 +144,9 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
this._unique = selected;
this.value = { ...this.value, unique: this._unique };
this._isCroppable = false;
this.#getSrc();
}
@@ -135,7 +165,13 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
override render() {
return html`
<umb-body-layout headline=${this.localize.term('defaultdialogs_selectMedia')}>
${this.#renderBody()}
<div id="layout">
${when(
this._isCroppable,
() => this.#renderImageCropper(),
() => this.#renderFilePreview(),
)}
</div>
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
<uui-button
@@ -148,30 +184,61 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
`;
}
#renderBody() {
#renderActions() {
return html`
<div id="layout">
<umb-image-cropper-editor-field
.value=${this._imageCropperValue}
?hideFocalPoint=${this._hideFocalPoint}
@change=${this.#onChange}>
<div id="actions" slot="actions">
<uui-button compact @click=${this.#openMediaPicker} label=${this.localize.term('mediaPicker_changeMedia')}>
<uui-icon name="icon-search"></uui-icon>${this.localize.term('mediaPicker_changeMedia')}
</uui-button>
<uui-button
compact
href=${this._editMediaPath + 'edit/' + this._unique}
label=${this.localize.term('mediaPicker_openMedia')}>
<uui-icon name="icon-out"></uui-icon>${this.localize.term('mediaPicker_openMedia')}
</uui-button>
</div>
</umb-image-cropper-editor-field>
<uui-button compact label=${this.localize.term('mediaPicker_changeMedia')} @click=${this.#openMediaPicker}>
<uui-icon name="icon-search"></uui-icon>
<umb-localize key="mediaPicker_changeMedia">Change Media Item</umb-localize>
</uui-button>
<uui-button
compact
label=${this.localize.term('mediaPicker_openMedia')}
href=${this._editMediaPath + 'edit/' + this._unique}>
<uui-icon name="icon-out"></uui-icon>
<umb-localize key="mediaPicker_openMedia">Open in Media Library</umb-localize>
</uui-button>
`;
}
#renderImageCropper() {
if (!this._imageCropperValue) return nothing;
return html`
<umb-image-cropper-editor-field
.value=${this._imageCropperValue}
?hideFocalPoint=${this._hideFocalPoint}
@change=${this.#onChange}>
<div slot="actions">${this.#renderActions()}</div>
</umb-image-cropper-editor-field>
`;
}
#renderFilePreview() {
return html`
<div id="main">
${when(
this._imageCropperValue?.src,
(path) => html`<umb-file-upload-preview .path=${path}></umb-file-upload-preview>`,
() => this.#renderFileNotFound(),
)}
</div>
<div id="actions">${this.#renderActions()}</div>
`;
}
#renderFileNotFound() {
const args = [this.localize.term('general_media')];
return html`
<div class="uui-text">
<h4>
<umb-localize key="entityDetail_notFoundTitle" .args=${args}>Item not found</umb-localize>
</h4>
<umb-localize key="entityDetail_notFoundDescription">The requested item could not be found.</umb-localize>
</div>
`;
}
static override styles = [
UmbTextStyles,
css`
#layout {
height: 100%;
@@ -179,35 +246,42 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
flex-direction: column;
justify-content: space-between;
}
umb-image-cropper-editor-field {
flex-grow: 1;
flex: 1;
}
#main {
flex: 1;
background-color: var(--uui-color-surface);
outline: 1px solid var(--uui-color-border);
}
#actions {
display: inline-flex;
gap: var(--uui-size-space-3);
}
uui-icon {
padding-right: var(--uui-size-3);
}
#options {
display: flex;
justify-content: center;
margin-top: 0.5rem;
uui-icon {
padding-right: var(--uui-size-1);
}
}
img {
max-width: 80%;
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill-opacity=".1"><path d="M50 0h50v50H50zM0 50h50v50H0z"/></svg>');
background-size: 10px 10px;
background-repeat: repeat;
.uui-text {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
`,
];
}
/** @deprecated Should be exported as `element` only; to be removed in Umbraco 18. */
export default UmbImageCropperEditorModalElement;
export { UmbImageCropperEditorModalElement as element };
declare global {
interface HTMLElementTagNameMap {
'umb-image-cropper-editor-modal': UmbImageCropperEditorModalElement;

View File

@@ -459,11 +459,6 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
padding-bottom: 5px; /** The modal is a bit jumpy due to the img card focus/hover border. This fixes the issue. */
}
/** TODO: Remove this fix when UUI gets upgrade to 1.3 */
umb-imaging-thumbnail {
pointer-events: none;
}
umb-icon {
font-size: var(--uui-size-8);
}

View File

@@ -30,10 +30,13 @@ export class UmbPropertyEditorUIUploadFieldElement extends UmbLitElement impleme
}
override render() {
return html`<umb-input-upload-field
@change="${this.#onChange}"
.allowedFileExtensions="${this._fileExtensions}"
.value=${this.value}></umb-input-upload-field>`;
return html`
<umb-input-upload-field
.allowedFileExtensions=${this._fileExtensions}
.value=${this.value}
@change=${this.#onChange}>
</umb-input-upload-field>
`;
}
}