diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/components/extension-slot/extension-slot.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/components/extension-slot/extension-slot.element.ts index 4baab15b28..069e41f5db 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/components/extension-slot/extension-slot.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/components/extension-slot/extension-slot.element.ts @@ -39,14 +39,14 @@ export class UmbExtensionSlotElement extends UmbLitElement { * */ @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 { * ext.meta.anyPropToFilter === 'foo'}> */ @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 { * */ @property({ type: Object, attribute: false }) - get props(): Record | undefined { - return this.#props; - } set props(newVal: Record | undefined) { this.#props = newVal; if (this.#extensionsController) { this.#extensionsController.properties = newVal; } } + get props(): Record | undefined { + return this.#props; + } #props?: Record = {}; @property({ type: String, attribute: 'default-element' }) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts index d5b99b79d6..60274dfda6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts @@ -44,6 +44,7 @@ export class UmbTemporaryFileConfigRepository extends UmbRepositoryBase implemen /** * Subscribe to the entire configuration. + * @returns {Observable} */ 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} */ part( part: Part, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts index b11ceceeb9..d4c69d01a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts @@ -99,14 +99,8 @@ export class UmbImagingThumbnailElement extends UmbLitElement { return when( this._thumbnailUrl, - () => - html`${this.alt}`, - () => html``, + (url) => html`${this.alt}`, + () => html``, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts index 486dada1ec..d0e5928405 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts @@ -11,7 +11,7 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< > { /** * The thumbnail items that are currently displayed in the collection. - * @deprecated Use the `` element instead. + * @deprecated Use the `` element instead. This will be removed in Umbraco 17. */ public readonly thumbnailItems = this.items; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts index f7a186fe97..627e82efce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts @@ -175,69 +175,91 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement { } protected renderActions() { - return html` + return html` + ${when( - !this.hideFocalPoint, - () => - html``, - )} `; + !this.hideFocalPoint && this.focalPoint.left !== 0.5 && this.focalPoint.top !== 0.5, + () => html` + + + ${this.localize.term('content_resetFocalPoint')} + + `, + )} + `; } protected renderSide() { if (!this.value || !this.crops) return; - return repeat( this.crops, (crop) => crop.alias + JSON.stringify(crop.coordinates), - (crop) => - html` this.onCropClick(crop)} + (crop) => html` + `, + .src=${this.source} + ?active=${this.currentCrop?.alias === crop.alias} + @click=${() => this.onCropClick(crop)}> + + `, ); } - 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 { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts index 8a5ad6a3fd..7e18a63eff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts @@ -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 { nothing} src=${this.src} alt="" /> diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts index 9f3b81c1d0..a0892a831c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts @@ -173,14 +173,14 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin< } #renderImageCropper() { - return html` - - ${this.localize.term('content_uploadClear')} - - `; + return html` + + + + Remove file(s) + + + `; } 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); + } + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/file-upload-preview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/file-upload-preview.element.ts new file mode 100644 index 0000000000..a3020e3352 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/file-upload-preview.element.ts @@ -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 = []; + + async #setPreviewAlias(): Promise { + 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` + manifest.alias === this._previewAlias}> + + `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-audio.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-audio.element.ts index f86c9afdaf..bf6c9fd93c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-audio.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-audio.element.ts @@ -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``; - - return html``; + const label = this.path.split('/').pop() ?? ''; + return html``; } static override readonly styles = [ @@ -23,6 +20,7 @@ export default class UmbInputUploadFieldAudioElement extends UmbLitElement { width: 999px; max-width: 100%; } + audio { width: 100%; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts index 3e349d20f4..427a95bc58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts @@ -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``; - - return html` - - `; + if (!this.path) return html``; + const fileExt = this.path.split('.').pop() ?? ''; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-image.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-image.element.ts index c64c2b7ef8..24f20b5cc2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-image.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-image.element.ts @@ -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``; - - const label = this.#label; - - return html` - ${label} - `; + const label = this.path.split('/').pop() ?? ''; + return html`${label}`; } 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%; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-svg.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-svg.element.ts index ae97a6924d..5ba66a4ce5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-svg.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-svg.element.ts @@ -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``; - - const label = this.#label; - - return html` - ${label} - `; + const label = this.path.split('/').pop() ?? ''; + return html`${label}`; } 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%; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-video.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-video.element.ts index 27898c8f98..0176d12248 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-video.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-video.element.ts @@ -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``; - + const label = this.path.split('/').pop() ?? ''; return html` -