Merge pull request #1840 from umbraco/feature/media-picker-filter-and-media-input-config
Feature: MediaPickerModal Filters & InputMedia Configuration
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './input-rich-media/index.js';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './input-rich-media.element.js';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user