Skip to content

Commit

Permalink
Merge pull request #2528 from umbraco/v15/feature/media-image-component
Browse files Browse the repository at this point in the history
Feature: initial work on media image component
  • Loading branch information
nielslyngsoe authored Nov 8, 2024
2 parents 443c83b + bff5119 commit c1346da
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand All @@ -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()
Expand All @@ -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;
Expand Down Expand Up @@ -168,6 +166,6 @@ export class UmbImagingThumbnailElement extends UmbLitElement {

declare global {
interface HTMLElementTagNameMap {
[ELEMENT_NAME]: UmbImagingThumbnailElement;
'umb-imaging-thumbnail': UmbImagingThumbnailElement;
}
}
1 change: 1 addition & 0 deletions src/packages/media/imaging/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './imaging-thumbnail.element.js';
export * from './media-image.element.js';
126 changes: 126 additions & 0 deletions src/packages/media/imaging/components/media-image.element.ts
Original file line number Diff line number Diff line change
@@ -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`<div id="loader"><uui-loader></uui-loader></div>`;
}

#renderThumbnail() {
if (this._isLoading) return nothing;

return when(
this._imageUrl,
() =>
html`<img
part="img"
src="${this._imageUrl}"
alt="${this.alt ?? ''}"
loading="${this.loading}"
draggable="false" />`,
() => html`<umb-icon id="icon" name="${this.icon}"></umb-icon>`,
);
}

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;
}
}
11 changes: 6 additions & 5 deletions src/packages/media/imaging/imaging.repository.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,13 +21,14 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi {

/**
* Requests the items for the given uniques
* @param {Array<string>} uniques
* @param imagingModel
* @param {Array<string>} uniques - The uniques
* @param {UmbImagingResizeModel} imagingModel - The imaging model
* @returns {Promise<{ data: UmbMediaUrlModel[] }>}
* @memberof UmbImagingRepository
*/
async requestResizedItems(
uniques: Array<string>,
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');
Expand Down Expand Up @@ -69,7 +70,7 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi {
* @memberof UmbImagingRepository
*/
async requestThumbnailUrls(uniques: Array<string>, height: number, width: number, mode = UmbImagingCropMode.MIN) {
const imagingModel: UmbImagingModel = { height, width, mode };
const imagingModel: UmbImagingResizeModel = { height, width, mode };
return this.requestResizedItems(uniques, imagingModel);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/packages/media/imaging/imaging.server.data.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -28,7 +28,7 @@ export class UmbImagingServerDataSource {
* @param imagingModel
* @memberof UmbImagingServerDataSource
*/
async getItems(uniques: Array<string>, imagingModel?: UmbImagingModel) {
async getItems(uniques: Array<string>, imagingModel?: UmbImagingResizeModel) {
if (!uniques.length) throw new Error('Uniques are missing');

const { data, error } = await tryExecuteAndNotify(
Expand Down
25 changes: 14 additions & 11 deletions src/packages/media/imaging/imaging.store.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,28 +14,30 @@ export class UmbImagingStore extends UmbContextBase<never> implements UmbApi {

/**
* Gets the data from the store.
* @param unique
* @param {string} unique - The media key
* @returns {Map<string, string> | undefined} - The data if it exists
*/
getData(unique: string) {
return this.#data.get(unique);
}

/**
* 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());
}
Expand All @@ -44,9 +46,10 @@ export class UmbImagingStore extends UmbContextBase<never> 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';
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/packages/media/imaging/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

0 comments on commit c1346da

Please sign in to comment.