diff --git a/src/packages/media/imaging/components/imaging-thumbnail.element.ts b/src/packages/media/imaging/components/imaging-thumbnail.element.ts
new file mode 100644
index 0000000000..81ae2ff903
--- /dev/null
+++ b/src/packages/media/imaging/components/imaging-thumbnail.element.ts
@@ -0,0 +1,173 @@
+import { UmbImagingCropMode } from '../types.js';
+import { UmbImagingRepository } from '../imaging.repository.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';
+
+const ELEMENT_NAME = 'umb-imaging-thumbnail';
+
+@customElement(ELEMENT_NAME)
+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.
+ */
+ @property()
+ unique = '';
+
+ /**
+ * The width of the thumbnail in pixels.
+ * @default 300
+ */
+ @property({ type: Number })
+ width = 300;
+
+ /**
+ * The height of the thumbnail in pixels.
+ * @default 300
+ */
+ @property({ type: Number })
+ height = 300;
+
+ /**
+ * The mode of the thumbnail.
+ * @remark The mode determines how the image is cropped.
+ * @enum {UmbImagingCropMode}
+ */
+ @property()
+ mode: UmbImagingCropMode = UmbImagingCropMode.MIN;
+
+ /**
+ * The alt text for the thumbnail.
+ */
+ @property()
+ alt = '';
+
+ /**
+ * The fallback icon for the thumbnail.
+ */
+ @property()
+ icon = 'icon-picture';
+
+ /**
+ * The `loading` state of the thumbnail.
+ * @enum {'lazy' | 'eager'}
+ * @default 'lazy'
+ */
+ @property()
+ loading: 'lazy' | 'eager' = 'lazy';
+
+ @state()
+ private _isLoading = true;
+
+ @state()
+ private _thumbnailUrl = '';
+
+ #imagingRepository = new UmbImagingRepository(this);
+
+ #intersectionObserver?: IntersectionObserver;
+
+ override render() {
+ return html` ${this.#renderThumbnail()} ${when(this._isLoading, () => this.#renderLoading())} `;
+ }
+
+ 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();
+ }
+
+ #renderLoading() {
+ return html`
`;
+ }
+
+ #renderThumbnail() {
+ if (this._isLoading) return nothing;
+
+ return when(
+ this._thumbnailUrl,
+ () =>
+ html``,
+ () => html``,
+ );
+ }
+
+ async #generateThumbnailUrl() {
+ const { data } = await this.#imagingRepository.requestThumbnailUrls(
+ [this.unique],
+ this.height,
+ this.width,
+ this.mode,
+ );
+
+ this._thumbnailUrl = data?.[0]?.url ?? '';
+ this._isLoading = false;
+ }
+
+ static override styles = [
+ UmbTextStyles,
+ css`
+ :host {
+ display: block;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ #loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ }
+
+ #figure {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+
+ background-image: url('data:image/svg+xml;charset=utf-8,');
+ background-size: 10px 10px;
+ background-repeat: repeat;
+ }
+
+ #icon {
+ width: 100%;
+ height: 100%;
+ font-size: var(--uui-size-8);
+ }
+ `,
+ ];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [ELEMENT_NAME]: UmbImagingThumbnailElement;
+ }
+}
diff --git a/src/packages/media/imaging/components/index.ts b/src/packages/media/imaging/components/index.ts
new file mode 100644
index 0000000000..60818ea906
--- /dev/null
+++ b/src/packages/media/imaging/components/index.ts
@@ -0,0 +1 @@
+export * from './imaging-thumbnail.element.js';
diff --git a/src/packages/media/imaging/imaging.repository.ts b/src/packages/media/imaging/imaging.repository.ts
index d6f7935283..c602078a52 100644
--- a/src/packages/media/imaging/imaging.repository.ts
+++ b/src/packages/media/imaging/imaging.repository.ts
@@ -1,7 +1,6 @@
-import type { UmbImagingModel } from './types.js';
+import { UmbImagingCropMode, type UmbImagingModel } from './types.js';
import { UmbImagingServerDataSource } from './imaging.server.data.js';
import { UMB_IMAGING_STORE_CONTEXT } from './imaging.store.token.js';
-import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
@@ -68,7 +67,7 @@ export class UmbImagingRepository extends UmbRepositoryBase implements UmbApi {
* @param {ImageCropModeModel} mode - The crop mode
* @memberof UmbImagingRepository
*/
- async requestThumbnailUrls(uniques: Array, height: number, width: number, mode = ImageCropModeModel.MIN) {
+ async requestThumbnailUrls(uniques: Array, height: number, width: number, mode = UmbImagingCropMode.MIN) {
const imagingModel: UmbImagingModel = { height, width, mode };
return this.requestResizedItems(uniques, imagingModel);
}
diff --git a/src/packages/media/imaging/index.ts b/src/packages/media/imaging/index.ts
index afd4abe3b5..d05152d5dc 100644
--- a/src/packages/media/imaging/index.ts
+++ b/src/packages/media/imaging/index.ts
@@ -1,2 +1,3 @@
+export * from './components/index.js';
export { UmbImagingRepository } from './imaging.repository.js';
export { UMB_IMAGING_REPOSITORY_ALIAS } from './constants.js';
diff --git a/src/packages/media/imaging/types.ts b/src/packages/media/imaging/types.ts
index 41df2f5b95..0411433515 100644
--- a/src/packages/media/imaging/types.ts
+++ b/src/packages/media/imaging/types.ts
@@ -1,7 +1,9 @@
-import type { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
+import { ImageCropModeModel as UmbImagingCropMode } from '@umbraco-cms/backoffice/external/backend-api';
+
+export { UmbImagingCropMode };
export interface UmbImagingModel {
height?: number;
width?: number;
- mode?: ImageCropModeModel;
+ mode?: UmbImagingCropMode;
}
diff --git a/src/packages/media/media/collection/media-collection.context.ts b/src/packages/media/media/collection/media-collection.context.ts
index 60ca5a16f5..4785fc9b8c 100644
--- a/src/packages/media/media/collection/media-collection.context.ts
+++ b/src/packages/media/media/collection/media-collection.context.ts
@@ -1,7 +1,5 @@
import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from './types.js';
import { UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js';
-import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
-import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -9,31 +7,14 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
UmbMediaCollectionItemModel,
UmbMediaCollectionFilterModel
> {
- #imagingRepository: UmbImagingRepository;
-
- #thumbnailItems = new UmbArrayState([], (x) => x.unique);
- public readonly thumbnailItems = this.#thumbnailItems.asObservable();
+ /**
+ * The thumbnail items that are currently displayed in the collection.
+ * @deprecated Use the `` element instead.
+ */
+ public readonly thumbnailItems = this.items;
constructor(host: UmbControllerHost) {
super(host, UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS);
- this.#imagingRepository = new UmbImagingRepository(host);
-
- this.observe(this.items, async (items) => {
- if (!items?.length) return;
-
- const { data } = await this.#imagingRepository.requestThumbnailUrls(
- items.map((m) => m.unique),
- 400,
- 400,
- );
-
- this.#thumbnailItems.setValue(
- items.map((item) => {
- const thumbnail = data?.find((m) => m.unique === item.unique)?.url;
- return { ...item, url: thumbnail };
- }),
- );
- });
}
}
diff --git a/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts
index 2345df4307..27042a9738 100644
--- a/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts
+++ b/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts
@@ -8,6 +8,8 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
+import '@umbraco-cms/backoffice/imaging';
+
@customElement('umb-media-grid-collection-view')
export class UmbMediaGridCollectionViewElement extends UmbLitElement {
@state()
@@ -52,7 +54,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading');
- this.observe(this.#collectionContext.thumbnailItems, (items) => (this._items = items), '_observeItems');
+ this.observe(this.#collectionContext.items, (items) => (this._items = items), '_observeItems');
this.observe(
this.#collectionContext.selection.selection,
@@ -127,13 +129,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
@selected=${() => this.#onSelect(item)}
@deselected=${() => this.#onDeselect(item)}
class="media-item">
- ${when(
- item.url,
- () => html``,
- () => html``,
- )}
-
-
+
`;
}
@@ -158,16 +154,6 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
grid-auto-rows: 200px;
gap: var(--uui-size-space-5);
}
-
- img {
- background-image: url('data:image/svg+xml;charset=utf-8,');
- background-size: 10px 10px;
- background-repeat: repeat;
- }
-
- umb-icon {
- font-size: var(--uui-size-8);
- }
`,
];
}
diff --git a/src/packages/media/media/components/input-media/input-media.element.ts b/src/packages/media/media/components/input-media/input-media.element.ts
index 7affdb7650..8377fa87d1 100644
--- a/src/packages/media/media/components/input-media/input-media.element.ts
+++ b/src/packages/media/media/components/input-media/input-media.element.ts
@@ -1,7 +1,6 @@
import type { UmbMediaCardItemModel } from '../../modals/index.js';
import type { UmbMediaItemModel } from '../../repository/index.js';
import { UmbMediaPickerContext } from './input-media.context.js';
-import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
import { css, customElement, html, ifDefined, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@@ -11,6 +10,8 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
+import '@umbraco-cms/backoffice/imaging';
+
const elementName = 'umb-input-media';
@customElement(elementName)
@@ -123,8 +124,6 @@ export class UmbInputMediaElement extends UmbFormControlMixin !this._cards.find((card) => card.unique === item.unique));
if (selectedItems?.length && !missingCards.length) return;
- if (!selectedItems?.length) {
- this._cards = [];
- return;
- }
-
- const uniques = selectedItems.map((x) => x.unique);
-
- const { data: thumbnails } = await this.#imagingRepository.requestThumbnailUrls(uniques, 400, 400);
-
- this._cards = selectedItems.map((item) => {
- const thumbnail = thumbnails?.find((x) => x.unique === item.unique);
- return {
- ...item,
- src: thumbnail?.url,
- };
- });
+ this._cards = selectedItems ?? [];
});
this.addValidator(
@@ -228,9 +212,10 @@ export class UmbInputMediaElement extends UmbFormControlMixin
- ${item.src
- ? html``
- : html``}
+
${this.#renderIsTrashed(item)}
item.mediaKey);
const { data: items } = await this.#itemRepository.requestItems(uniques);
- const { data: thumbnails } = await this.#imagingRepository.requestThumbnailUrls(uniques, 400, 400);
this._cards = this.items.map((item) => {
const media = items?.find((x) => x.unique === item.mediaKey);
- const thumbnail = thumbnails?.find((x) => x.unique === item.mediaKey);
return {
unique: item.key,
media: item.mediaKey,
name: media?.name ?? '',
- src: thumbnail?.url,
icon: media?.mediaType?.icon,
isTrashed: media?.isTrashed ?? false,
};
@@ -366,9 +362,10 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
const href = this._routeBuilder?.({ key: item.unique });
return html`
- ${item.src
- ? html``
- : html``}
+
${this.#renderIsTrashed(item)}
{
#mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure
#mediaItemRepository = new UmbMediaItemRepository(this); // used to search
- #imagingRepository = new UmbImagingRepository(this); // used to get image renditions
#dataType?: { unique: string };
- @state()
- private _filter: (item: UmbMediaCardItemModel) => boolean = () => true;
-
@state()
private _selectableFilter: (item: UmbMediaCardItemModel) => boolean = () => true;
@@ -62,7 +58,6 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<
override async connectedCallback(): Promise {
super.connectedCallback();
- if (this.data?.filter) this._filter = this.data?.filter;
if (this.data?.pickableFilter) this._selectableFilter = this.data?.pickableFilter;
if (this.data?.startNode) {
@@ -87,27 +82,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<
take: 100,
});
- this.#mediaItemsCurrentFolder = await this.#mapMediaUrls(data?.items ?? []);
+ this.#mediaItemsCurrentFolder = data?.items ?? [];
this.#filterMediaItems();
}
- async #mapMediaUrls(items: Array): Promise> {
- if (!items.length) return [];
-
- const { data } = await this.#imagingRepository.requestThumbnailUrls(
- items.map((item) => item.unique),
- 400,
- 400,
- );
-
- return items
- .map((item): UmbMediaCardItemModel => {
- const src = data?.find((media) => media.unique === item.unique)?.url;
- return { ...item, src };
- })
- .filter((item) => this._filter(item));
- }
-
#onOpen(item: UmbMediaCardItemModel) {
this._currentMediaEntity = {
name: item.name,
@@ -152,7 +130,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<
}
// Map urls for search results as we are going to show for all folders (as long they aren't trashed).
- this._mediaFilteredList = await this.#mapMediaUrls(data.filter((found) => found.isTrashed === false));
+ this._mediaFilteredList = data.filter((found) => found.isTrashed === false);
}
#debouncedSearch = debounce(() => {
@@ -240,9 +218,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<
@deselected=${() => this.#onDeselected(item)}
?selected=${this.value?.selection?.find((value) => value === item.unique)}
?selectable=${!disabled}>
- ${item.src
- ? html``
- : html``}
+
`;
}