Merge remote-tracking branch 'origin/main' into feature/property-editor-color-picker
This commit is contained in:
@@ -14,5 +14,7 @@ import './section/section-sidebar/section-sidebar.element';
|
||||
import './section/section.element';
|
||||
import './tree/tree.element';
|
||||
import './workspace/workspace-content/workspace-content.element';
|
||||
import './input-media-picker/input-media-picker.element';
|
||||
import './input-document-picker/input-document-picker.element';
|
||||
import './empty-state/empty-state.element';
|
||||
import './color-picker/color-picker.element';
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { UUIBooleanInputEvent } from '@umbraco-ui/uui';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-input-checkbox-list')
|
||||
export class UmbInputCheckboxListElement extends FormControlMixin(UmbLitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
uui-checkbox {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of items.
|
||||
*/
|
||||
@property()
|
||||
list?: [];
|
||||
|
||||
private _selectedKeys: Array<string> = [];
|
||||
public get selectedKeys(): Array<string> {
|
||||
return this._selectedKeys;
|
||||
}
|
||||
public set selectedKeys(keys: Array<string>) {
|
||||
this._selectedKeys = keys;
|
||||
super.value = keys.join(',');
|
||||
}
|
||||
|
||||
@property()
|
||||
public set value(keysString: string) {
|
||||
if (keysString !== this._value) {
|
||||
this.selectedKeys = keysString.split(/[ ,]+/);
|
||||
}
|
||||
}
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _setSelection(e: UUIBooleanInputEvent) {
|
||||
e.stopPropagation();
|
||||
if (e.target.checked) this.selectedKeys = [...this.selectedKeys, e.target.value];
|
||||
else this._removeFromSelection(this.selectedKeys.findIndex((key) => e.target.value === key));
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
private _removeFromSelection(index: number) {
|
||||
if (index == -1) return;
|
||||
const keys = [...this.selectedKeys];
|
||||
keys.splice(index, 1);
|
||||
this.selectedKeys = keys;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.list) return nothing;
|
||||
return html`<form>
|
||||
<uui-form @change="${this._setSelection}">
|
||||
${repeat(this.list, (item) => item.key, this.renderCheckbox)}
|
||||
</uui-form>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
renderCheckbox(item: any) {
|
||||
return html`<uui-checkbox value="${item.key}" label="${item.label}"></uui-checkbox>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbInputCheckboxListElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-input-checkbox-list': UmbInputCheckboxListElement;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* This is a minimum amount of selected items in this input.
|
||||
* @type {number}
|
||||
@@ -122,7 +121,10 @@ export class UmbInputDocumentPickerElement extends FormControlMixin(UmbLitElemen
|
||||
|
||||
private _openPicker() {
|
||||
// We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data:
|
||||
const modalHandler = this._modalService?.contentPicker({ multiple: true, selection: [...this._selectedKeys] });
|
||||
const modalHandler = this._modalService?.contentPicker({
|
||||
multiple: this.max === 1 ? false : true,
|
||||
selection: [...this._selectedKeys],
|
||||
});
|
||||
modalHandler?.onClose().then(({ selection }: any) => {
|
||||
this._setSelection(selection);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbInputDocumentPickerElement } from './input-document-picker.element';
|
||||
import { defaultA11yConfig } from '@umbraco-cms/test-utils';
|
||||
describe('UmbPropertyEditorUINumberRangeElement', () => {
|
||||
let element: UmbInputDocumentPickerElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html` <umb-input-document-picker></umb-input-document-picker> `);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbInputDocumentPickerElement);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
|
||||
import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal';
|
||||
import {
|
||||
MediaTreeItem,
|
||||
UmbMediaTreeStore,
|
||||
UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN,
|
||||
} from '../../../../backoffice/media/media/media.tree.store';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import type { FolderTreeItem } from '@umbraco-cms/backend-api';
|
||||
import type { UmbObserverController } from '@umbraco-cms/observable-api';
|
||||
|
||||
@customElement('umb-input-media-picker')
|
||||
export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: grid;
|
||||
gap: var(--uui-size-space-3);
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
}
|
||||
#add-button {
|
||||
text-align: center;
|
||||
min-height: 160px;
|
||||
}
|
||||
uui-icon {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* This is a minimum amount of selected items in this input.
|
||||
* @type {number}
|
||||
* @attr
|
||||
* @default undefined
|
||||
*/
|
||||
@property({ type: Number })
|
||||
min?: number;
|
||||
|
||||
/**
|
||||
* Min validation message.
|
||||
* @type {boolean}
|
||||
* @attr
|
||||
* @default
|
||||
*/
|
||||
@property({ type: String, attribute: 'min-message' })
|
||||
minMessage = 'This field need more items';
|
||||
|
||||
/**
|
||||
* This is a maximum amount of selected items in this input.
|
||||
* @type {number}
|
||||
* @attr
|
||||
* @default undefined
|
||||
*/
|
||||
@property({ type: Number })
|
||||
max?: number;
|
||||
|
||||
/**
|
||||
* Max validation message.
|
||||
* @type {boolean}
|
||||
* @attr
|
||||
* @default
|
||||
*/
|
||||
@property({ type: String, attribute: 'min-message' })
|
||||
maxMessage = 'This field exceeds the allowed amount of items';
|
||||
|
||||
// TODO: do we need both selectedKeys and value? If we just use value we follow the same pattern as native form controls.
|
||||
private _selectedKeys: Array<string> = [];
|
||||
public get selectedKeys(): Array<string> {
|
||||
return this._selectedKeys;
|
||||
}
|
||||
public set selectedKeys(keys: Array<string>) {
|
||||
this._selectedKeys = keys;
|
||||
super.value = keys.join(',');
|
||||
this._observePickedMedias();
|
||||
}
|
||||
|
||||
@property()
|
||||
public set value(keysString: string) {
|
||||
if (keysString !== this._value) {
|
||||
this.selectedKeys = keysString.split(/[ ,]+/);
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private _items?: Array<MediaTreeItem>;
|
||||
|
||||
private _modalService?: UmbModalService;
|
||||
private _mediaStore?: UmbMediaTreeStore;
|
||||
private _pickedItemsObserver?: UmbObserverController<FolderTreeItem>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addValidator(
|
||||
'rangeUnderflow',
|
||||
() => this.minMessage,
|
||||
() => !!this.min && this._selectedKeys.length < this.min
|
||||
);
|
||||
this.addValidator(
|
||||
'rangeOverflow',
|
||||
() => this.maxMessage,
|
||||
() => !!this.max && this._selectedKeys.length > this.max
|
||||
);
|
||||
|
||||
this.consumeContext(UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => {
|
||||
this._mediaStore = instance;
|
||||
this._observePickedMedias();
|
||||
});
|
||||
this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => {
|
||||
this._modalService = instance;
|
||||
});
|
||||
}
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _observePickedMedias() {
|
||||
this._pickedItemsObserver?.destroy();
|
||||
|
||||
if (!this._mediaStore) return;
|
||||
|
||||
// TODO: consider changing this to the list data endpoint when it is available
|
||||
this._pickedItemsObserver = this.observe(this._mediaStore.getTreeItems(this._selectedKeys), (items) => {
|
||||
this._items = items;
|
||||
});
|
||||
}
|
||||
|
||||
private _openPicker() {
|
||||
// We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data:
|
||||
const modalHandler = this._modalService?.mediaPicker({
|
||||
multiple: this.max === 1 ? false : true,
|
||||
selection: [...this._selectedKeys],
|
||||
});
|
||||
modalHandler?.onClose().then(({ selection }: any) => {
|
||||
this._setSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
private _removeItem(item: FolderTreeItem) {
|
||||
const modalHandler = this._modalService?.confirm({
|
||||
color: 'danger',
|
||||
headline: `Remove ${item.name}?`,
|
||||
content: 'Are you sure you want to remove this item',
|
||||
confirmLabel: 'Remove',
|
||||
});
|
||||
|
||||
modalHandler?.onClose().then(({ confirmed }) => {
|
||||
if (confirmed) {
|
||||
const newSelection = this._selectedKeys.filter((value) => value !== item.key);
|
||||
this._setSelection(newSelection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setSelection(newSelection: Array<string>) {
|
||||
this.selectedKeys = newSelection;
|
||||
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
|
||||
console.log(this._items);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${this._items?.map((item) => this._renderItem(item))} ${this._renderButton()} `;
|
||||
}
|
||||
private _renderButton() {
|
||||
if (this.max == 1 && this._items && this._items.length > 0) return;
|
||||
return html`<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">
|
||||
<uui-icon name="umb:add"></uui-icon>
|
||||
Add
|
||||
</uui-button>`;
|
||||
}
|
||||
|
||||
private _renderItem(item: FolderTreeItem) {
|
||||
// TODO: remove when we have a way to handle trashed items
|
||||
const tempItem = item as FolderTreeItem & { isTrashed: boolean };
|
||||
|
||||
return html`
|
||||
<uui-card-media
|
||||
name=${ifDefined(item.name === null ? undefined : item.name)}
|
||||
detail=${ifDefined(item.key)}
|
||||
file-ext="jpg">
|
||||
${tempItem.isTrashed ? html` <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> ` : nothing}
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button label="Copy media">
|
||||
<uui-icon name="umb:documents"></uui-icon>
|
||||
</uui-button>
|
||||
<uui-button @click=${() => this._removeItem(item)} label="Remove media ${item.name}">
|
||||
<uui-icon name="umb:trash"></uui-icon>
|
||||
</uui-button>
|
||||
</uui-action-bar>
|
||||
</uui-card-media>
|
||||
`;
|
||||
//TODO: <uui-button-inline-create vertical></uui-button-inline-create>
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbInputMediaPickerElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-input-media-picker': UmbInputMediaPickerElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbInputMediaPickerElement } from './input-media-picker.element';
|
||||
import { defaultA11yConfig } from '@umbraco-cms/test-utils';
|
||||
describe('UmbPropertyEditorUINumberRangeElement', () => {
|
||||
let element: UmbInputMediaPickerElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html` <umb-input-media-picker></umb-input-media-picker> `);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbInputMediaPickerElement);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import '../../../components/input-checkbox-list/input-checkbox-list.element';
|
||||
import { UmbInputCheckboxListElement } from '../../../components/input-checkbox-list/input-checkbox-list.element';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import type { DataTypePropertyData } from '@umbraco-cms/models';
|
||||
|
||||
/**
|
||||
* @element umb-property-editor-ui-checkbox-list
|
||||
@@ -10,14 +13,37 @@ import { UmbLitElement } from '@umbraco-cms/element';
|
||||
export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement {
|
||||
static styles = [UUITextStyles];
|
||||
|
||||
@property()
|
||||
value = '';
|
||||
private _value: Array<string> = [];
|
||||
@property({ type: Array })
|
||||
public get value(): Array<string> {
|
||||
return this._value;
|
||||
}
|
||||
public set value(value: Array<string>) {
|
||||
this._value = value || [];
|
||||
}
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public config = [];
|
||||
public set config(config: Array<DataTypePropertyData>) {
|
||||
const listData = config.find((x) => x.alias === 'itemList');
|
||||
|
||||
if (!listData) return;
|
||||
this._list = listData.value;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _list: [] = [];
|
||||
|
||||
private _onChange(event: CustomEvent) {
|
||||
this.value = (event.target as UmbInputCheckboxListElement).selectedKeys;
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
console.log(this._value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>umb-property-editor-ui-checkbox-list</div>`;
|
||||
return html`<umb-input-checkbox-list
|
||||
@change="${this._onChange}"
|
||||
.selectedKeys="${this._value}"
|
||||
.list="${this._list}"></umb-input-checkbox-list>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { UmbInputDocumentPickerElement } from '../../../components/input-document-picker/input-document-picker.element';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import type { UmbInputDocumentPickerElement } from 'src/backoffice/shared/components/input-document-picker/input-document-picker.element';
|
||||
import '../../../components/input-document-picker/input-document-picker.element';
|
||||
import type { DataTypePropertyData } from '@umbraco-cms/models';
|
||||
|
||||
@customElement('umb-property-editor-ui-document-picker')
|
||||
|
||||
@@ -1,23 +1,53 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { UmbInputMediaPickerElement } from '../../../../../backoffice/shared/components/input-media-picker/input-media-picker.element';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import type { DataTypePropertyData } from '@umbraco-cms/models';
|
||||
|
||||
/**
|
||||
* @element umb-property-editor-ui-media-picker
|
||||
*/
|
||||
@customElement('umb-property-editor-ui-media-picker')
|
||||
export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement {
|
||||
static styles = [UUITextStyles];
|
||||
private _value: Array<string> = [];
|
||||
|
||||
@property()
|
||||
value = '';
|
||||
@property({ type: Array })
|
||||
public get value(): Array<string> {
|
||||
return this._value;
|
||||
}
|
||||
public set value(value: Array<string>) {
|
||||
this._value = value || [];
|
||||
}
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public config = [];
|
||||
public set config(config: Array<DataTypePropertyData>) {
|
||||
const validationLimit = config.find((x) => x.alias === 'validationLimit');
|
||||
if (!validationLimit) return;
|
||||
|
||||
this._limitMin = (validationLimit?.value as any).min;
|
||||
this._limitMax = (validationLimit?.value as any).max;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _limitMin?: number;
|
||||
@state()
|
||||
private _limitMax?: number;
|
||||
|
||||
private _onChange(event: CustomEvent) {
|
||||
this.value = (event.target as UmbInputMediaPickerElement).selectedKeys;
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>umb-property-editor-ui-media-picker</div>`;
|
||||
return html`
|
||||
<umb-input-media-picker
|
||||
@change=${this._onChange}
|
||||
.selectedKeys=${this._value}
|
||||
.min=${this._limitMin}
|
||||
.max=${this._limitMax}
|
||||
>Add</umb-input-media-picker
|
||||
>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,15 @@ export const data: Array<DataTypeDetails> = [
|
||||
isFolder: false,
|
||||
propertyEditorModelAlias: 'Umbraco.CheckboxList',
|
||||
propertyEditorUIAlias: 'Umb.PropertyEditorUI.CheckboxList',
|
||||
data: [],
|
||||
data: [
|
||||
{
|
||||
alias: 'itemList',
|
||||
value: [
|
||||
{ label: 'Label 1', key: '123' },
|
||||
{ label: 'Label 2', key: '456' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Block List',
|
||||
|
||||
Reference in New Issue
Block a user