diff --git a/src/packages/media/imaging/components/imaging-thumbnail.element.ts b/src/packages/media/imaging/components/imaging-thumbnail.element.ts index 81ae2ff903..b11ceceeb9 100644 --- a/src/packages/media/imaging/components/imaging-thumbnail.element.ts +++ b/src/packages/media/imaging/components/imaging-thumbnail.element.ts @@ -4,13 +4,11 @@ import { css, customElement, html, nothing, property, state, when } from '@umbra import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -const ELEMENT_NAME = 'umb-imaging-thumbnail'; - -@customElement(ELEMENT_NAME) +@customElement('umb-imaging-thumbnail') export class UmbImagingThumbnailElement extends UmbLitElement { /** * The unique identifier for the media item. - * @remark This is also known as the media key and is used to fetch the resource. + * @description This is also known as the media key and is used to fetch the resource. */ @property() unique = ''; @@ -31,7 +29,7 @@ export class UmbImagingThumbnailElement extends UmbLitElement { /** * The mode of the thumbnail. - * @remark The mode determines how the image is cropped. + * @description The mode determines how the image is cropped. * @enum {UmbImagingCropMode} */ @property() @@ -55,7 +53,7 @@ export class UmbImagingThumbnailElement extends UmbLitElement { * @default 'lazy' */ @property() - loading: 'lazy' | 'eager' = 'lazy'; + loading: (typeof HTMLImageElement)['prototype']['loading'] = 'lazy'; @state() private _isLoading = true; @@ -168,6 +166,6 @@ export class UmbImagingThumbnailElement extends UmbLitElement { declare global { interface HTMLElementTagNameMap { - [ELEMENT_NAME]: UmbImagingThumbnailElement; + 'umb-imaging-thumbnail': UmbImagingThumbnailElement; } } diff --git a/src/packages/media/imaging/components/index.ts b/src/packages/media/imaging/components/index.ts index 60818ea906..a2ce8114af 100644 --- a/src/packages/media/imaging/components/index.ts +++ b/src/packages/media/imaging/components/index.ts @@ -1 +1,2 @@ export * from './imaging-thumbnail.element.js'; +export * from './media-image.element.js'; diff --git a/src/packages/media/imaging/components/media-image.element.ts b/src/packages/media/imaging/components/media-image.element.ts new file mode 100644 index 0000000000..c81596b112 --- /dev/null +++ b/src/packages/media/imaging/components/media-image.element.ts @@ -0,0 +1,126 @@ +import { UmbMediaUrlRepository } from '../../media/repository/index.js'; +import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-media-image') +export class UmbMediaImageElement extends UmbLitElement { + /** + * The unique identifier for the media item. + * @description This is also known as the media key and is used to fetch the resource. + */ + @property() + unique?: string; + + /** + * The alt text for the thumbnail. + */ + @property() + alt?: string; + + /** + * The fallback icon for the thumbnail. + */ + @property() + icon = 'icon-picture'; + + /** + * The `loading` state of the thumbnail. + * @enum {'lazy' | 'eager'} + * @default 'lazy' + */ + @property() + loading: (typeof HTMLImageElement)['prototype']['loading'] = 'lazy'; + + @state() + private _isLoading = true; + + @state() + private _imageUrl = ''; + + #mediaRepository = new UmbMediaUrlRepository(this); + + #intersectionObserver?: IntersectionObserver; + + override connectedCallback() { + super.connectedCallback(); + + if (this.loading === 'lazy') { + this.#intersectionObserver = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + this.#generateThumbnailUrl(); + this.#intersectionObserver?.disconnect(); + } + }); + this.#intersectionObserver.observe(this); + } else { + this.#generateThumbnailUrl(); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.#intersectionObserver?.disconnect(); + } + + async #generateThumbnailUrl() { + if (!this.unique) throw new Error('Unique is missing'); + const { data } = await this.#mediaRepository.requestItems([this.unique]); + + this._imageUrl = data?.[0]?.url ?? ''; + this._isLoading = false; + } + + override render() { + return html` ${this.#renderThumbnail()} ${when(this._isLoading, () => this.#renderLoading())} `; + } + + #renderLoading() { + return html`
`; + } + + #renderThumbnail() { + if (this._isLoading) return nothing; + + return when( + this._imageUrl, + () => + html`${this.alt ?? ''}`, + () => html``, + ); + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: contents; + } + + #loader { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + } + + #icon { + width: 100%; + height: 100%; + font-size: var(--uui-size-8); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-image': UmbMediaImageElement; + } +} diff --git a/src/packages/media/imaging/imaging.repository.ts b/src/packages/media/imaging/imaging.repository.ts index 4672f801d9..ae2d6ff134 100644 --- a/src/packages/media/imaging/imaging.repository.ts +++ b/src/packages/media/imaging/imaging.repository.ts @@ -1,4 +1,4 @@ -import { UmbImagingCropMode, type UmbImagingModel } from './types.js'; +import { UmbImagingCropMode, type UmbImagingResizeModel } from './types.js'; import { UmbImagingServerDataSource } from './imaging.server.data.js'; import { UMB_IMAGING_STORE_CONTEXT } from './imaging.store.token.js'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; @@ -21,13 +21,14 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi { /** * Requests the items for the given uniques - * @param {Array} uniques - * @param imagingModel + * @param {Array} uniques - The uniques + * @param {UmbImagingResizeModel} imagingModel - The imaging model + * @returns {Promise<{ data: UmbMediaUrlModel[] }>} * @memberof UmbImagingRepository */ async requestResizedItems( uniques: Array, - imagingModel?: UmbImagingModel, + imagingModel?: UmbImagingResizeModel, ): Promise<{ data: UmbMediaUrlModel[] }> { if (!uniques.length) throw new Error('Uniques are missing'); if (!this.#dataStore) throw new Error('Data store is missing'); @@ -69,7 +70,7 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi { * @memberof UmbImagingRepository */ async requestThumbnailUrls(uniques: Array, height: number, width: number, mode = UmbImagingCropMode.MIN) { - const imagingModel: UmbImagingModel = { height, width, mode }; + const imagingModel: UmbImagingResizeModel = { height, width, mode }; return this.requestResizedItems(uniques, imagingModel); } } diff --git a/src/packages/media/imaging/imaging.server.data.ts b/src/packages/media/imaging/imaging.server.data.ts index 30ec2b411b..e53b9b1ee7 100644 --- a/src/packages/media/imaging/imaging.server.data.ts +++ b/src/packages/media/imaging/imaging.server.data.ts @@ -1,4 +1,4 @@ -import type { UmbImagingModel } from './types.js'; +import type { UmbImagingResizeModel } from './types.js'; import { ImagingService, type MediaUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbMediaUrlModel } from '@umbraco-cms/backoffice/media'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -28,7 +28,7 @@ export class UmbImagingServerDataSource { * @param imagingModel * @memberof UmbImagingServerDataSource */ - async getItems(uniques: Array, imagingModel?: UmbImagingModel) { + async getItems(uniques: Array, imagingModel?: UmbImagingResizeModel) { if (!uniques.length) throw new Error('Uniques are missing'); const { data, error } = await tryExecuteAndNotify( diff --git a/src/packages/media/imaging/imaging.store.ts b/src/packages/media/imaging/imaging.store.ts index 85af357997..5bff323f4f 100644 --- a/src/packages/media/imaging/imaging.store.ts +++ b/src/packages/media/imaging/imaging.store.ts @@ -1,5 +1,5 @@ import { UMB_IMAGING_STORE_CONTEXT } from './imaging.store.token.js'; -import type { UmbImagingModel } from './types.js'; +import type { UmbImagingResizeModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; @@ -14,7 +14,8 @@ export class UmbImagingStore extends UmbContextBase implements UmbApi { /** * Gets the data from the store. - * @param unique + * @param {string} unique - The media key + * @returns {Map | undefined} - The data if it exists */ getData(unique: string) { return this.#data.get(unique); @@ -22,20 +23,21 @@ export class UmbImagingStore extends UmbContextBase implements UmbApi { /** * Gets a specific crop if it exists. - * @param unique - * @param data + * @param {string} unique - The media key + * @param {string} data - The resize configuration + * @returns {string | undefined} - The crop if it exists */ - getCrop(unique: string, data?: UmbImagingModel) { + getCrop(unique: string, data?: UmbImagingResizeModel) { return this.#data.get(unique)?.get(this.#generateCropKey(data)); } /** * Adds a new crop to the store. - * @param unique - * @param urlInfo - * @param data + * @param {string} unique - The media key + * @param {string} urlInfo - The URL of the crop + * @param { | undefined} data - The resize configuration */ - addCrop(unique: string, urlInfo: string, data?: UmbImagingModel) { + addCrop(unique: string, urlInfo: string, data?: UmbImagingResizeModel) { if (!this.#data.has(unique)) { this.#data.set(unique, new Map()); } @@ -44,9 +46,10 @@ export class UmbImagingStore extends UmbContextBase implements UmbApi { /** * Generates a unique key for the crop based on the width, height and mode. - * @param data + * @param {UmbImagingResizeModel} data - The resize configuration + * @returns {string} - The crop key */ - #generateCropKey(data?: UmbImagingModel) { + #generateCropKey(data?: UmbImagingResizeModel) { return data ? `${data.width}x${data.height};${data.mode}` : 'generic'; } } diff --git a/src/packages/media/imaging/types.ts b/src/packages/media/imaging/types.ts index 0411433515..6d848cca61 100644 --- a/src/packages/media/imaging/types.ts +++ b/src/packages/media/imaging/types.ts @@ -2,8 +2,14 @@ import { ImageCropModeModel as UmbImagingCropMode } from '@umbraco-cms/backoffic export { UmbImagingCropMode }; -export interface UmbImagingModel { +export interface UmbImagingResizeModel { height?: number; width?: number; mode?: UmbImagingCropMode; } + +/** + * @deprecated use `UmbImagingResizeModel` instead + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbImagingModel extends UmbImagingResizeModel {}