Merge pull request #2528 from umbraco/v15/feature/media-image-component
Feature: initial work on media image component
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './imaging-thumbnail.element.js';
|
||||
export * from './media-image.element.js';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<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');
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>, imagingModel?: UmbImagingModel) {
|
||||
async getItems(uniques: Array<string>, imagingModel?: UmbImagingResizeModel) {
|
||||
if (!uniques.length) throw new Error('Uniques are missing');
|
||||
|
||||
const { data, error } = await tryExecuteAndNotify(
|
||||
|
||||
@@ -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<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);
|
||||
@@ -22,20 +23,21 @@ export class UmbImagingStore extends UmbContextBase<never> 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<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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user