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:
@@ -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' })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user