Merge pull request #1840 from umbraco/feature/media-picker-filter-and-media-input-config

Feature: MediaPickerModal Filters & InputMedia Configuration
This commit is contained in:
Lee Kelleher
2024-05-21 10:41:32 +01:00
committed by GitHub
15 changed files with 281 additions and 148 deletions

View File

@@ -153,6 +153,13 @@ 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,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill-opacity=".1"><path d="M50 0h50v50H50zM0 50h50v50H0z"/></svg>');
background-size: 10px 10px;
background-repeat: repeat;
}
umb-icon {
font-size: var(--uui-size-8);
}

View File

@@ -1,11 +1,11 @@
import { UMB_MEDIA_PICKER_MODAL, type UmbMediaCardItemModel } from '../../modals/index.js';
import {
UMB_MEDIA_PICKER_MODAL,
type UmbMediaPickerModalData,
type UmbMediaPickerModalValue,
type UmbMediaCardItemModel,
} from '../../modals/index.js';
import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js';
import type { UmbMediaItemModel } from '../../repository/item/types.js';
import type { UmbMediaTreeItemModel } from '../../tree/index.js';
import type {
UmbMediaTreePickerModalData,
UmbMediaTreePickerModalValue,
} from '../../tree/media-tree-picker-modal.token.js';
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
@@ -14,9 +14,9 @@ import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api
export class UmbMediaPickerContext extends UmbPickerInputContext<
UmbMediaItemModel,
UmbMediaTreeItemModel,
UmbMediaTreePickerModalData,
UmbMediaTreePickerModalValue
UmbMediaItemModel,
UmbMediaPickerModalData<UmbMediaItemModel>,
UmbMediaPickerModalValue
> {
#imagingRepository: UmbImagingRepository;
@@ -40,14 +40,7 @@ export class UmbMediaPickerContext extends UmbPickerInputContext<
this.#cardItems.setValue(
selectedItems.map((item) => {
const url = data?.find((x) => x.unique === item.unique)?.url;
return {
icon: item.mediaType.icon,
name: item.name,
unique: item.unique,
isTrashed: item.isTrashed,
entityType: item.entityType,
url,
};
return { ...item, url };
}),
);
});

View File

@@ -8,7 +8,6 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbModalRouteRegistrationController, UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbUploadableFileModel } from '@umbraco-cms/backoffice/media';
@customElement('umb-input-media')
export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') {
@@ -22,9 +21,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
identifier: 'Umb.SorterIdentifier.InputMedia',
itemSelector: 'uui-card-media',
containerSelector: '.container',
/** TODO: This component probably needs some grid-like logic for resolve placement... [LI] */
resolvePlacement: () => false,
onChange: ({ model }) => {
this.selection = model;
this.dispatchEvent(new UmbChangeEvent());
},
});
@@ -89,6 +89,12 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
@property({ type: Boolean })
showOpenButton?: boolean;
@property({ type: String })
startNode = '';
@property({ type: Boolean })
multiple = false;
@property()
public set value(idsString: string) {
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
@@ -99,10 +105,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
}
@state()
private _editMediaPath = '';
protected editMediaPath = '';
@state()
private _items?: Array<UmbMediaCardItemModel>;
protected items?: Array<UmbMediaCardItemModel>;
#pickerContext = new UmbMediaPickerContext(this);
@@ -115,12 +121,12 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
return { data: { entityType: 'media', preset: {} } };
})
.observeRouteBuilder((routeBuilder) => {
this._editMediaPath = routeBuilder({});
this.editMediaPath = routeBuilder({});
});
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
this.observe(this.#pickerContext.cardItems, (cardItems) => {
this._items = cardItems;
this.items = cardItems;
});
this.addValidator(
@@ -149,43 +155,31 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
#openPicker() {
this.#pickerContext.openPicker({
hideTreeRoot: true,
multiple: this.multiple,
startNode: this.startNode,
pickableFilter: this.#pickableFilter,
});
}
async #onUploadCompleted(e: CustomEvent) {
const completed = e.detail?.completed as Array<UmbUploadableFileModel>;
const uploaded = completed.map((file) => file.unique);
this.selection = [...this.selection, ...uploaded];
this.dispatchEvent(new UmbChangeEvent());
protected onRemove(item: UmbMediaCardItemModel) {
this.#pickerContext.requestRemoveItem(item.unique);
}
render() {
return html`${this.#renderDropzone()}
<div class="container">${this.#renderItems()} ${this.#renderAddButton()}</div>`;
}
#renderDropzone() {
if (this._items && this._items.length >= this.max) return;
return html`<umb-dropzone
id="dropzone"
?multiple=${this.max === 1}
@change=${this.#onUploadCompleted}></umb-dropzone>`;
return html`<div class="container">${this.#renderItems()} ${this.#renderAddButton()}</div>`;
}
#renderItems() {
if (!this._items?.length) return;
if (!this.items?.length) return;
return html`${repeat(
this._items,
this.items,
(item) => item.unique,
(item) => this.#renderItem(item),
(item) => this.renderItem(item),
)}`;
}
#renderAddButton() {
if (this._items && this.max && this._items.length >= this.max) return;
if ((this.items && this.max && this.items.length >= this.max) || (this.items?.length && !this.multiple)) return;
return html`
<uui-button
id="btn-add"
@@ -198,22 +192,21 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
`;
}
#renderItem(item: UmbMediaCardItemModel) {
protected renderItem(item: UmbMediaCardItemModel) {
return html`
<uui-card-media name=${ifDefined(item.name === null ? undefined : item.name)} detail=${ifDefined(item.unique)}>
<uui-card-media
name=${ifDefined(item.name === null ? undefined : item.name)}
detail=${ifDefined(item.unique)}
href="${this.editMediaPath}edit/${item.unique}">
${item.url
? html`<img src=${item.url} alt=${item.name} />`
: html`<umb-icon name=${ifDefined(item.icon)}></umb-icon>`}
${this.#renderIsTrashed(item)}
: html`<umb-icon name=${ifDefined(item.mediaType.icon)}></umb-icon>`}
${this.renderIsTrashed(item)}
<uui-action-bar slot="actions">
${this.#renderOpenButton(item)}
<uui-button label="Copy media" look="secondary">
<uui-icon name="icon-documents"></uui-icon>
</uui-button>
<uui-button
label=${this.localize.term('general_remove')}
look="secondary"
@click=${() => this.#pickerContext.requestRemoveItem(item.unique)}
label="Remove media ${item.name}">
@click=${() => this.onRemove(item)}>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
</uui-action-bar>
@@ -221,7 +214,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
`;
}
#renderIsTrashed(item: UmbMediaCardItemModel) {
protected renderIsTrashed(item: UmbMediaCardItemModel) {
if (!item.isTrashed) return;
return html`
<uui-tag size="s" slot="tag" color="danger">
@@ -230,18 +223,6 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
`;
}
#renderOpenButton(item: UmbMediaCardItemModel) {
if (!this.showOpenButton) return;
return html`
<uui-button
compact
href="${this._editMediaPath}edit/${item.unique}"
label=${this.localize.term('general_edit') + ` ${item.name}`}>
<uui-icon name="icon-edit"></uui-icon>
</uui-button>
`;
}
static styles = [
css`
:host {
@@ -250,8 +231,8 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '')
.container {
display: grid;
gap: var(--uui-size-space-3);
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-template-rows: repeat(auto-fill, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-auto-rows: 150px;
}
#btn-add {

View File

@@ -1,24 +1,39 @@
import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js';
import { UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js';
import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js';
import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js';
import type { UmbDropzoneElement } from '../../dropzone/dropzone.element.js';
import type { UmbMediaItemModel } from '../../repository/index.js';
import type { UmbMediaCardItemModel, UmbMediaPathModel } from './types.js';
import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js';
import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, repeat, ifDefined, query } from '@umbraco-cms/backoffice/external/lit';
import { debounce } from '@umbraco-cms/backoffice/utils';
import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UMB_CONTENT_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE };
@customElement('umb-media-picker-modal')
export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPickerModalData, UmbMediaPickerModalValue> {
export class UmbMediaPickerModalElement extends UmbModalBaseElement<
UmbMediaPickerModalData<unknown>,
UmbMediaPickerModalValue
> {
#mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure
#mediaUrlRepository = new UmbMediaUrlRepository(this); // used to get urls
#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;
#mediaItemsCurrentFolder: Array<UmbMediaCardItemModel> = [];
@state()
@@ -33,9 +48,25 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
@state()
private _currentMediaEntity: UmbMediaPathModel = root;
@query('#dropzone')
private _dropzone!: UmbDropzoneElement;
constructor() {
super();
this.consumeContext(UMB_CONTENT_PROPERTY_CONTEXT, (context) => {
this.observe(context.dataType, (dataType) => {
this.#dataType = dataType;
});
});
}
async connectedCallback(): Promise<void> {
super.connectedCallback();
if (this.data?.filter) this._filter = this.data?.filter;
if (this.data?.pickableFilter) this._selectableFilter = this.data?.pickableFilter;
if (this.data?.startNode) {
const { data } = await this.#mediaItemRepository.requestItems([this.data.startNode]);
@@ -53,6 +84,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
unique: this._currentMediaEntity.unique,
entityType: this._currentMediaEntity.entityType,
},
dataType: this.#dataType,
skip: 0,
take: 100,
});
@@ -69,17 +101,12 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
{ height: 400, width: 400, mode: ImageCropModeModel.MIN },
);
return items.map((item): UmbMediaCardItemModel => {
const url = data?.find((media) => media.unique === item.unique)?.url;
return {
name: item.name,
unique: item.unique,
url,
icon: item.mediaType.icon,
entityType: item.entityType,
isTrashed: item.isTrashed,
};
});
return items
.map((item): UmbMediaCardItemModel => {
const url = data?.find((media) => media.unique === item.unique)?.url;
return { ...item, url };
})
.filter((item) => this._filter(item));
}
#onOpen(item: UmbMediaCardItemModel) {
@@ -129,9 +156,13 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
this._mediaFilteredList = await this.#mapMediaUrls(data.filter((found) => found.isTrashed === false));
}
#debouncedSearch = debounce(() => {
this.#filterMediaItems();
}, 500);
#onSearch(e: UUIInputEvent) {
this._searchQuery = (e.target.value as string).toLocaleLowerCase();
this.#filterMediaItems();
this.#debouncedSearch();
}
#onPathChange(e: CustomEvent) {
@@ -176,37 +207,43 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
}
#renderToolbar() {
return html`<div id="toolbar">
<umb-media-picker-create-item .node=${this._currentMediaEntity.unique}></umb-media-picker-create-item>
<div id="search">
<uui-input
label=${this.localize.term('general_search')}
placeholder=${this.localize.term('placeholders_search')}
@change=${this.#onSearch}>
<uui-icon slot="prepend" name="icon-search"></uui-icon>
</uui-input>
/**<umb-media-picker-create-item .node=${this._currentMediaEntity.unique}></umb-media-picker-create-item>
* We cannot route to a workspace without the media picker modal is a routeable. Using regular upload button for now... */
return html`
<div id="toolbar">
<div id="search">
<uui-input
label=${this.localize.term('general_search')}
placeholder=${this.localize.term('placeholders_search')}
@input=${this.#onSearch}>
<uui-icon slot="prepend" name="icon-search"></uui-icon>
</uui-input>
<uui-checkbox
@change=${() => (this._searchOnlyThisFolder = !this._searchOnlyThisFolder)}
label=${this.localize.term('general_excludeFromSubFolders')}></uui-checkbox>
</div>
<uui-button
@click=${() => this._dropzone.browse()}
label=${this.localize.term('general_upload')}
look="primary"></uui-button>
</div>
<uui-button label="TODO" compact @click=${() => alert('TODO: Show media items as list/grid')}
><uui-icon name="icon-grid"></uui-icon
></uui-button>
</div>`;
`;
}
// Where should this be placed, without it looking terrible?
// <uui-checkbox @change=${() => (this._searchOnlyThisFolder = !this._searchOnlyThisFolder)} label=${this.localize.term('general_excludeFromSubFolders')}></uui-checkbox>
#renderCard(item: UmbMediaCardItemModel) {
const disabled = !this._selectableFilter(item);
return html`
<uui-card-media
.name=${item.name ?? 'Unnamed Media'}
class=${ifDefined(disabled ? 'not-allowed' : undefined)}
.name=${item.name}
@open=${() => this.#onOpen(item)}
@selected=${() => this.#onSelected(item)}
@deselected=${() => this.#onDeselected(item)}
?selected=${this.value?.selection?.find((value) => value === item.unique)}
selectable>
?selectable=${!disabled}>
${item.url
? html`<img src=${item.url} alt=${ifDefined(item.name)} />`
: html`<umb-icon .name=${item.icon}></umb-icon>`}
: html`<umb-icon .name=${item.mediaType.icon}></umb-icon>`}
</uui-card-media>
`;
}
@@ -241,8 +278,8 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
}
#media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 200px;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-auto-rows: 150px;
gap: var(--uui-size-space-5);
padding-bottom: 5px; /** The modal is a bit jumpy due to the img card focus/hover border. This fixes the issue. */
}
@@ -259,6 +296,11 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
#actions {
max-width: 100%;
}
.not-allowed {
cursor: not-allowed;
opacity: 0.5;
}
`,
];
}

View File

@@ -1,15 +1,17 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbMediaPickerModalData {
export interface UmbMediaPickerModalData<ItemType> {
startNode?: string | null;
multiple?: boolean;
pickableFilter?: (item: ItemType) => boolean;
filter?: (item: ItemType) => boolean;
}
export type UmbMediaPickerModalValue = {
selection: string[];
};
export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken<UmbMediaPickerModalData, UmbMediaPickerModalValue>(
export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken<UmbMediaPickerModalData<unknown>, UmbMediaPickerModalValue>(
'Umb.Modal.MediaPicker',
{
modal: {

View File

@@ -1,12 +1,7 @@
import type { UmbMediaEntityType } from '../../entity.js';
import type { UmbMediaItemModel } from '../../repository/index.js';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbMediaCardItemModel {
name: string;
unique: string;
entityType: UmbMediaEntityType;
isTrashed: boolean;
icon: string;
export interface UmbMediaCardItemModel extends UmbMediaItemModel {
url?: string;
}

View File

@@ -33,7 +33,7 @@ export class UmbPropertyEditorUIMediaEntityPickerElement extends UmbLitElement i
return undefined;
}
#onChange(event: { target: UmbInputMediaElement }) {
#onChange(event: CustomEvent & { target: UmbInputMediaElement }) {
this.value = event.target.selection?.join(',') ?? null;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}

View File

@@ -0,0 +1 @@
export * from './input-rich-media/index.js';

View File

@@ -0,0 +1 @@
export * from './input-rich-media.element.js';

View File

@@ -0,0 +1,69 @@
import type { UmbCropModel } from '../../index.js';
import type { UmbMediaCardItemModel } from '../../../../modals/index.js';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { customElement, html, ifDefined, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
import type { UmbUploadableFileModel } from '@umbraco-cms/backoffice/media';
const elementName = 'umb-input-rich-media';
@customElement(elementName)
export class UmbInputRichMediaElement extends UmbInputMediaElement {
@property({ type: Boolean })
focalPointEnabled = false;
@property({ type: Array })
crops?: Array<UmbCropModel>;
async #onUploadCompleted(e: CustomEvent) {
const completed = e.detail?.completed as Array<UmbUploadableFileModel>;
const uploaded = completed.map((file) => file.unique);
this.selection = [...this.selection, ...uploaded];
this.dispatchEvent(new UmbChangeEvent());
}
render() {
return html`${this.#renderDropzone()} ${super.render()}`;
}
#renderDropzone() {
if (this.items && this.items.length >= this.max) return;
return html`<umb-dropzone @change=${this.#onUploadCompleted}></umb-dropzone>`;
}
protected renderItem(item: UmbMediaCardItemModel) {
return html`
<uui-card-media
name=${ifDefined(item.name === null ? undefined : item.name)}
detail=${ifDefined(item.unique)}
href="${this.editMediaPath}edit/${item.unique}"
@open=${() => {
alert('open media crops modal');
}}>
${item.url
? html`<img src=${item.url} alt=${item.name} />`
: html`<umb-icon name=${ifDefined(item.mediaType.icon)}></umb-icon>`}
${this.renderIsTrashed(item)}
<uui-action-bar slot="actions">
<uui-button label="Copy media" look="secondary">
<uui-icon name="icon-documents"></uui-icon>
</uui-button>
<uui-button
label=${this.localize.term('general_remove')}
look="secondary"
@click=${() => this.onRemove(item)}>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
</uui-action-bar>
</uui-card-media>
`;
}
}
export { UmbInputRichMediaElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbInputRichMediaElement;
}
}

View File

@@ -5,5 +5,12 @@ export type UmbMediaPickerPropertyValue = {
mediaKey: string;
mediaTypeAlias: string;
focalPoint: { left: number; top: number } | null;
crops: Array<{ alias: string; width: number; height: number }>;
crops: Array<UmbCropModel>;
};
export interface UmbCropModel {
label: string;
alias: string;
width: number;
height: number;
}

View File

@@ -1,18 +1,19 @@
import type { UmbInputMediaElement } from '../../components/input-media/input-media.element.js';
import '../../components/input-media/input-media.element.js';
import type { UmbMediaPickerPropertyValue } from './index.js';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import {
UmbPropertyValueChangeEvent,
type UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbInputRichMediaElement } from './components/input-rich-media/input-rich-media.element.js';
import type { UmbCropModel, UmbMediaPickerPropertyValue } from './index.js';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import './components/input-rich-media/input-rich-media.element.js';
/**
* @element umb-property-editor-ui-media-picker
*/
@customElement('umb-property-editor-ui-media-picker')
export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ attribute: false })
@@ -21,36 +22,57 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme
this._items = this.value ? this.value.map((x) => x.mediaKey) : [];
}
//TODO: Add support for document specific crops. The server side already supports this.
public get value() {
return this.#value;
}
@state()
private _startNode: string = '';
@state()
private _focalPointEnabled: boolean = false;
@state()
private _crops: Array<UmbCropModel> = [];
@state()
private _allowedMediaTypes: Array<string> = [];
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
const validationLimit = config?.getByAlias('validationLimit');
if (!validationLimit) return;
if (!config) return;
const minMax: Record<string, number> = validationLimit.value as any;
this._multiple = Boolean(config.getValueByAlias('multiple'));
this._startNode = config.getValueByAlias<string>('startNodeId') ?? '';
this._focalPointEnabled = Boolean(config.getValueByAlias('enableFocalPoint'));
this._crops = config?.getValueByAlias<Array<UmbCropModel>>('crops') ?? [];
this._limitMin = minMax.min ?? 0;
this._limitMax = minMax.max ?? Infinity;
const filter = config.getValueByAlias<string>('filter');
this._allowedMediaTypes = filter?.split(',') ?? [];
const minMax = config.getValueByAlias<NumberRangeValueType>('validationLimit');
this._limitMin = minMax?.min ?? 0;
this._limitMax = minMax?.max ?? Infinity;
}
public get config() {
return undefined;
}
@state()
private _multiple: boolean = false;
@state()
_items: Array<string> = [];
@state()
private _limitMin: number = 0;
@state()
private _limitMax: number = Infinity;
#value: Array<UmbMediaPickerPropertyValue> = [];
#onChange(event: CustomEvent) {
const selection = (event.target as UmbInputMediaElement).selection;
#onChange(event: CustomEvent & { target: UmbInputRichMediaElement }) {
const selection = event.target.selection;
const result = selection.map((mediaKey) => {
return {
@@ -69,12 +91,17 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme
render() {
return html`
<umb-input-media
<umb-input-rich-media
@change=${this.#onChange}
?multiple=${this._multiple}
.allowedContentTypeIds=${this._allowedMediaTypes}
.startNode=${this._startNode}
.focalPointEnabled=${this._focalPointEnabled}
.crops=${this._crops}
.selection=${this._items}
.min=${this._limitMin}
.max=${this._limitMax}>
</umb-input-media>
</umb-input-rich-media>
`;
}
}

View File

@@ -1,13 +1,23 @@
import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js';
import { UmbMediaTreeServerDataSource } from './media-tree.server.data-source.js';
import type { UmbMediaTreeItemModel, UmbMediaTreeRootModel } from './types.js';
import type {
UmbMediaTreeChildrenOfRequestArgs,
UmbMediaTreeItemModel,
UmbMediaTreeRootItemsRequestArgs,
UmbMediaTreeRootModel,
} from './types.js';
import { UMB_MEDIA_TREE_STORE_CONTEXT } from './media-tree.store.js';
import { UmbTreeRepositoryBase } from '@umbraco-cms/backoffice/tree';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMediaTreeRepository
extends UmbTreeRepositoryBase<UmbMediaTreeItemModel, UmbMediaTreeRootModel>
extends UmbTreeRepositoryBase<
UmbMediaTreeItemModel,
UmbMediaTreeRootModel,
UmbMediaTreeRootItemsRequestArgs,
UmbMediaTreeChildrenOfRequestArgs
>
implements UmbApi
{
constructor(host: UmbControllerHost) {

View File

@@ -229,7 +229,10 @@ export class UmbWorkspacePackageBuilderElement extends UmbLitElement {
return html`
<umb-property-layout label="Media">
<div slot="editor">
<umb-input-media .selection=${this._package.mediaIds ?? []} @change=${this.#onMediaChange}></umb-input-media>
<umb-input-media
multiple
.selection=${this._package.mediaIds ?? []}
@change=${this.#onMediaChange}></umb-input-media>
<uui-checkbox
label="Include child nodes"
.checked=${this._package.mediaLoadChildNodes ?? false}

View File

@@ -6,12 +6,7 @@ import type { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from '@umbraco-cms
import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce';
import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file';
import { UmbId } from '@umbraco-cms/backoffice/id';
import {
sizeImageInEditor,
uploadBlobImages,
UMB_MEDIA_TREE_PICKER_MODAL,
UMB_MEDIA_PICKER_MODAL,
} from '@umbraco-cms/backoffice/media';
import { sizeImageInEditor, uploadBlobImages, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media';
interface MediaPickerTargetData {
altText?: string;