Skip to content

Commit c1346da

Browse files
authored
Merge pull request #2528 from umbraco/v15/feature/media-image-component
Feature: initial work on media image component
2 parents 443c83b + bff5119 commit c1346da

7 files changed

+161
-26
lines changed

src/packages/media/imaging/components/imaging-thumbnail.element.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ import { css, customElement, html, nothing, property, state, when } from '@umbra
44
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
55
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
66

7-
const ELEMENT_NAME = 'umb-imaging-thumbnail';
8-
9-
@customElement(ELEMENT_NAME)
7+
@customElement('umb-imaging-thumbnail')
108
export class UmbImagingThumbnailElement extends UmbLitElement {
119
/**
1210
* The unique identifier for the media item.
13-
* @remark This is also known as the media key and is used to fetch the resource.
11+
* @description This is also known as the media key and is used to fetch the resource.
1412
*/
1513
@property()
1614
unique = '';
@@ -31,7 +29,7 @@ export class UmbImagingThumbnailElement extends UmbLitElement {
3129

3230
/**
3331
* The mode of the thumbnail.
34-
* @remark The mode determines how the image is cropped.
32+
* @description The mode determines how the image is cropped.
3533
* @enum {UmbImagingCropMode}
3634
*/
3735
@property()
@@ -55,7 +53,7 @@ export class UmbImagingThumbnailElement extends UmbLitElement {
5553
* @default 'lazy'
5654
*/
5755
@property()
58-
loading: 'lazy' | 'eager' = 'lazy';
56+
loading: (typeof HTMLImageElement)['prototype']['loading'] = 'lazy';
5957

6058
@state()
6159
private _isLoading = true;
@@ -168,6 +166,6 @@ export class UmbImagingThumbnailElement extends UmbLitElement {
168166

169167
declare global {
170168
interface HTMLElementTagNameMap {
171-
[ELEMENT_NAME]: UmbImagingThumbnailElement;
169+
'umb-imaging-thumbnail': UmbImagingThumbnailElement;
172170
}
173171
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './imaging-thumbnail.element.js';
2+
export * from './media-image.element.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { UmbMediaUrlRepository } from '../../media/repository/index.js';
2+
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
3+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
4+
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
5+
6+
@customElement('umb-media-image')
7+
export class UmbMediaImageElement extends UmbLitElement {
8+
/**
9+
* The unique identifier for the media item.
10+
* @description This is also known as the media key and is used to fetch the resource.
11+
*/
12+
@property()
13+
unique?: string;
14+
15+
/**
16+
* The alt text for the thumbnail.
17+
*/
18+
@property()
19+
alt?: string;
20+
21+
/**
22+
* The fallback icon for the thumbnail.
23+
*/
24+
@property()
25+
icon = 'icon-picture';
26+
27+
/**
28+
* The `loading` state of the thumbnail.
29+
* @enum {'lazy' | 'eager'}
30+
* @default 'lazy'
31+
*/
32+
@property()
33+
loading: (typeof HTMLImageElement)['prototype']['loading'] = 'lazy';
34+
35+
@state()
36+
private _isLoading = true;
37+
38+
@state()
39+
private _imageUrl = '';
40+
41+
#mediaRepository = new UmbMediaUrlRepository(this);
42+
43+
#intersectionObserver?: IntersectionObserver;
44+
45+
override connectedCallback() {
46+
super.connectedCallback();
47+
48+
if (this.loading === 'lazy') {
49+
this.#intersectionObserver = new IntersectionObserver((entries) => {
50+
if (entries[0].isIntersecting) {
51+
this.#generateThumbnailUrl();
52+
this.#intersectionObserver?.disconnect();
53+
}
54+
});
55+
this.#intersectionObserver.observe(this);
56+
} else {
57+
this.#generateThumbnailUrl();
58+
}
59+
}
60+
61+
override disconnectedCallback() {
62+
super.disconnectedCallback();
63+
this.#intersectionObserver?.disconnect();
64+
}
65+
66+
async #generateThumbnailUrl() {
67+
if (!this.unique) throw new Error('Unique is missing');
68+
const { data } = await this.#mediaRepository.requestItems([this.unique]);
69+
70+
this._imageUrl = data?.[0]?.url ?? '';
71+
this._isLoading = false;
72+
}
73+
74+
override render() {
75+
return html` ${this.#renderThumbnail()} ${when(this._isLoading, () => this.#renderLoading())} `;
76+
}
77+
78+
#renderLoading() {
79+
return html`<div id="loader"><uui-loader></uui-loader></div>`;
80+
}
81+
82+
#renderThumbnail() {
83+
if (this._isLoading) return nothing;
84+
85+
return when(
86+
this._imageUrl,
87+
() =>
88+
html`<img
89+
part="img"
90+
src="${this._imageUrl}"
91+
alt="${this.alt ?? ''}"
92+
loading="${this.loading}"
93+
draggable="false" />`,
94+
() => html`<umb-icon id="icon" name="${this.icon}"></umb-icon>`,
95+
);
96+
}
97+
98+
static override styles = [
99+
UmbTextStyles,
100+
css`
101+
:host {
102+
display: contents;
103+
}
104+
105+
#loader {
106+
display: flex;
107+
justify-content: center;
108+
align-items: center;
109+
height: 100%;
110+
width: 100%;
111+
}
112+
113+
#icon {
114+
width: 100%;
115+
height: 100%;
116+
font-size: var(--uui-size-8);
117+
}
118+
`,
119+
];
120+
}
121+
122+
declare global {
123+
interface HTMLElementTagNameMap {
124+
'umb-media-image': UmbMediaImageElement;
125+
}
126+
}

src/packages/media/imaging/imaging.repository.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UmbImagingCropMode, type UmbImagingModel } from './types.js';
1+
import { UmbImagingCropMode, type UmbImagingResizeModel } from './types.js';
22
import { UmbImagingServerDataSource } from './imaging.server.data.js';
33
import { UMB_IMAGING_STORE_CONTEXT } from './imaging.store.token.js';
44
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
@@ -21,13 +21,14 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi {
2121

2222
/**
2323
* Requests the items for the given uniques
24-
* @param {Array<string>} uniques
25-
* @param imagingModel
24+
* @param {Array<string>} uniques - The uniques
25+
* @param {UmbImagingResizeModel} imagingModel - The imaging model
26+
* @returns {Promise<{ data: UmbMediaUrlModel[] }>}
2627
* @memberof UmbImagingRepository
2728
*/
2829
async requestResizedItems(
2930
uniques: Array<string>,
30-
imagingModel?: UmbImagingModel,
31+
imagingModel?: UmbImagingResizeModel,
3132
): Promise<{ data: UmbMediaUrlModel[] }> {
3233
if (!uniques.length) throw new Error('Uniques are missing');
3334
if (!this.#dataStore) throw new Error('Data store is missing');
@@ -69,7 +70,7 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi {
6970
* @memberof UmbImagingRepository
7071
*/
7172
async requestThumbnailUrls(uniques: Array<string>, height: number, width: number, mode = UmbImagingCropMode.MIN) {
72-
const imagingModel: UmbImagingModel = { height, width, mode };
73+
const imagingModel: UmbImagingResizeModel = { height, width, mode };
7374
return this.requestResizedItems(uniques, imagingModel);
7475
}
7576
}

src/packages/media/imaging/imaging.server.data.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { UmbImagingModel } from './types.js';
1+
import type { UmbImagingResizeModel } from './types.js';
22
import { ImagingService, type MediaUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
33
import type { UmbMediaUrlModel } from '@umbraco-cms/backoffice/media';
44
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -28,7 +28,7 @@ export class UmbImagingServerDataSource {
2828
* @param imagingModel
2929
* @memberof UmbImagingServerDataSource
3030
*/
31-
async getItems(uniques: Array<string>, imagingModel?: UmbImagingModel) {
31+
async getItems(uniques: Array<string>, imagingModel?: UmbImagingResizeModel) {
3232
if (!uniques.length) throw new Error('Uniques are missing');
3333

3434
const { data, error } = await tryExecuteAndNotify(

src/packages/media/imaging/imaging.store.ts

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { UMB_IMAGING_STORE_CONTEXT } from './imaging.store.token.js';
2-
import type { UmbImagingModel } from './types.js';
2+
import type { UmbImagingResizeModel } from './types.js';
33
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
44
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
55
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
@@ -14,28 +14,30 @@ export class UmbImagingStore extends UmbContextBase<never> implements UmbApi {
1414

1515
/**
1616
* Gets the data from the store.
17-
* @param unique
17+
* @param {string} unique - The media key
18+
* @returns {Map<string, string> | undefined} - The data if it exists
1819
*/
1920
getData(unique: string) {
2021
return this.#data.get(unique);
2122
}
2223

2324
/**
2425
* Gets a specific crop if it exists.
25-
* @param unique
26-
* @param data
26+
* @param {string} unique - The media key
27+
* @param {string} data - The resize configuration
28+
* @returns {string | undefined} - The crop if it exists
2729
*/
28-
getCrop(unique: string, data?: UmbImagingModel) {
30+
getCrop(unique: string, data?: UmbImagingResizeModel) {
2931
return this.#data.get(unique)?.get(this.#generateCropKey(data));
3032
}
3133

3234
/**
3335
* Adds a new crop to the store.
34-
* @param unique
35-
* @param urlInfo
36-
* @param data
36+
* @param {string} unique - The media key
37+
* @param {string} urlInfo - The URL of the crop
38+
* @param { | undefined} data - The resize configuration
3739
*/
38-
addCrop(unique: string, urlInfo: string, data?: UmbImagingModel) {
40+
addCrop(unique: string, urlInfo: string, data?: UmbImagingResizeModel) {
3941
if (!this.#data.has(unique)) {
4042
this.#data.set(unique, new Map());
4143
}
@@ -44,9 +46,10 @@ export class UmbImagingStore extends UmbContextBase<never> implements UmbApi {
4446

4547
/**
4648
* Generates a unique key for the crop based on the width, height and mode.
47-
* @param data
49+
* @param {UmbImagingResizeModel} data - The resize configuration
50+
* @returns {string} - The crop key
4851
*/
49-
#generateCropKey(data?: UmbImagingModel) {
52+
#generateCropKey(data?: UmbImagingResizeModel) {
5053
return data ? `${data.width}x${data.height};${data.mode}` : 'generic';
5154
}
5255
}

src/packages/media/imaging/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { ImageCropModeModel as UmbImagingCropMode } from '@umbraco-cms/backoffic
22

33
export { UmbImagingCropMode };
44

5-
export interface UmbImagingModel {
5+
export interface UmbImagingResizeModel {
66
height?: number;
77
width?: number;
88
mode?: UmbImagingCropMode;
99
}
10+
11+
/**
12+
* @deprecated use `UmbImagingResizeModel` instead
13+
*/
14+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
15+
export interface UmbImagingModel extends UmbImagingResizeModel {}

0 commit comments

Comments
 (0)