Merge branch 'main' into feature-Folder-Repository-Base

This commit is contained in:
Mads Rasmussen
2024-01-15 19:18:05 +01:00
47 changed files with 1054 additions and 443 deletions

View File

@@ -3,7 +3,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
type: 'dashboard',
name: 'Example Dataset Workspace View',
name: 'Example Dataset Dashboard',
alias: 'example.dashboard.dataset',
element: () => import('./dataset-dashboard.js'),
weight: 900,

View File

@@ -0,0 +1,5 @@
# Property Dataset Dashboard Example
This example demonstrates the how to setup the Sorter.
This example can still NOT sort between two groups. This will come later.

View File

@@ -0,0 +1,15 @@
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
type: 'dashboard',
name: 'Example Sorter Dashboard',
alias: 'example.dashboard.dataset',
element: () => import('./sorter-dashboard.js'),
weight: 900,
meta: {
label: 'Sorter example',
pathname: 'sorter-example',
},
},
];

View File

@@ -0,0 +1,72 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { ModelEntryType } from './sorter-group.js';
import './sorter-group.js';
@customElement('example-sorter-dashboard')
export class ExampleSorterDashboard extends UmbElementMixin(LitElement) {
groupOneItems: ModelEntryType[] = [
{
name: 'Apple',
},
{
name: 'Banana',
},
{
name: 'Pear',
},
{
name: 'Pineapple',
},
{
name: 'Lemon',
},
];
groupTwoItems: ModelEntryType[] = [
{
name: 'DXP',
},
{
name: 'H5YR',
},
{
name: 'UUI',
},
];
render() {
return html`
<uui-box class="uui-text">
<div class="outer-wrapper">
<h5>Notice this example still only support single group of Sorter.</h5>
<example-sorter-group .items=${this.groupOneItems}></example-sorter-group>
<example-sorter-group .items=${this.groupTwoItems}></example-sorter-group>
</div>
</uui-box>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
.outer-wrapper {
display: flex;
}
`,
];
}
export default ExampleSorterDashboard;
declare global {
interface HTMLElementTagNameMap {
'example-sorter-dashboard': ExampleSorterDashboard;
}
}

View File

@@ -0,0 +1,118 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, LitElement, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import './sorter-item.js';
import ExampleSorterItem from './sorter-item.js';
export type ModelEntryType = {
name: string;
};
const SORTER_CONFIG: UmbSorterConfig<ModelEntryType, ExampleSorterItem> = {
compareElementToModel: (element, model) => {
return element.name === model.name;
},
querySelectModelToElement: (container, modelEntry) => {
return container.querySelector("example-sorter-item[name='" + modelEntry.name + "']");
},
identifier: 'string-that-identifies-all-example-sorters',
itemSelector: 'example-sorter-item',
containerSelector: '.sorter-container',
};
@customElement('example-sorter-group')
export class ExampleSorterGroup extends UmbElementMixin(LitElement) {
@property({ type: Array, attribute: false })
public get items(): ModelEntryType[] {
return this._items;
}
public set items(value: ModelEntryType[]) {
this._items = value;
this.#sorter.setModel(this._items);
}
private _items: ModelEntryType[] = [];
#sorter = new UmbSorterController<ModelEntryType, ExampleSorterItem>(this, {
...SORTER_CONFIG,
/*performItemInsert: ({ item, newIndex }) => {
const oldValue = this._items;
//console.log('inserted', item.name, 'at', newIndex, ' ', this._items.map((x) => x.name).join(', '));
const newItems = [...this._items];
newItems.splice(newIndex, 0, item);
this.items = newItems;
this.requestUpdate('_items', oldValue);
return true;
},
performItemRemove: ({ item }) => {
const oldValue = this._items;
//console.log('removed', item.name, 'at', indexToMove, ' ', this._items.map((x) => x.name).join(', '));
const indexToMove = this._items.findIndex((x) => x.name === item.name);
const newItems = [...this._items];
newItems.splice(indexToMove, 1);
this.items = newItems;
this.requestUpdate('_items', oldValue);
return true;
},
performItemMove: ({ item, newIndex, oldIndex }) => {
const oldValue = this._items;
//console.log('move', item.name, 'from', oldIndex, 'to', newIndex, ' ', this._items.map((x) => x.name).join(', '));
const newItems = [...this._items];
newItems.splice(oldIndex, 1);
if (oldIndex <= newIndex) {
newIndex--;
}
newItems.splice(newIndex, 0, item);
this.items = newItems;
this.requestUpdate('_items', oldValue);
return true;
},*/
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
this.requestUpdate('_items', oldValue);
},
});
removeItem = (item: ModelEntryType) => {
this.items = this._items.filter((r) => r.name !== item.name);
};
render() {
return html`
<div class="sorter-container">
${repeat(
this.items,
(item) => item.name,
(item) =>
html`<example-sorter-item name=${item.name}>
<button @click=${() => this.removeItem(item)}>Delete</button>
</example-sorter-item>`,
)}
</div>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
width: 100%;
}
.sorter-placeholder {
opacity: 0.2;
}
`,
];
}
export default ExampleSorterGroup;
declare global {
interface HTMLElementTagNameMap {
'example-sorter-group': ExampleSorterGroup;
}
}

View File

@@ -0,0 +1,47 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, LitElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
@customElement('example-sorter-item')
export class ExampleSorterItem extends UmbElementMixin(LitElement) {
@property({ type: String, reflect: true })
name: string = '';
@property({ type: Boolean, reflect: true, attribute: 'drag-placeholder' })
umbDragPlaceholder = false;
render() {
return html`
${this.name}
<img src="https://picsum.photos/seed/${this.name}/400/400" style="width:120px;" />
<slot></slot>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--uui-size-layout-1);
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
margin-bottom: 3px;
}
:host([drag-placeholder]) {
opacity: 0.2;
}
`,
];
}
export default ExampleSorterItem;
declare global {
interface HTMLElementTagNameMap {
'example-sorter-item': ExampleSorterItem;
}
}

View File

@@ -34,7 +34,6 @@
"./localization": "./dist-cms/packages/core/localization/index.js",
"./macro": "./dist-cms/packages/core/macro/index.js",
"./menu": "./dist-cms/packages/core/menu/index.js",
"./meta": "./dist-cms/packages/core/meta/index.js",
"./modal": "./dist-cms/packages/core/modal/index.js",
"./notification": "./dist-cms/packages/core/notification/index.js",
"./picker-input": "./dist-cms/packages/core/picker-input/index.js",

View File

@@ -28,7 +28,7 @@ export class UmbInputSliderElement extends FormControlMixin(UmbLitElement) {
#onChange(e: UUISliderEvent) {
e.stopPropagation();
super.value = e.target.value;
this.value = e.target.value;
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}

View File

@@ -193,8 +193,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
this._tinyConfig = {
autoresize_bottom_margin: 10,
body_class: 'umb-rte',
//see https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#cache_suffix
cache_suffix: `?umb__rnd=${umbMeta.clientVersion}`,
contextMenu: false,
inline_boundaries_selector: 'a[href],code,.mce-annotation,.umb-embed-holder,.umb-macro-holder',
menubar: false,

View File

@@ -1,9 +1,8 @@
import { UmbInputDocumentPickerRootElement } from '@umbraco-cms/backoffice/document';
import { html, customElement, property, css, state } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, css, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
//import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export type UmbTreePickerSource = {
type?: UmbTreePickerSourceType;
@@ -65,25 +64,22 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
];
#onTypeChange(event: UUISelectEvent) {
//console.log('onTypeChange');
event.stopPropagation();
this.type = event.target.value as UmbTreePickerSource['type'];
this.nodeId = '';
// TODO: Appears that the event gets bubbled up. Will need to review. [LK]
//this.dispatchEvent(new UmbChangeEvent());
this.dispatchEvent(new UmbChangeEvent());
}
#onIdChange(event: CustomEvent) {
//console.log('onIdChange', event.target);
switch (this.type) {
case 'content':
this.nodeId = (<UmbInputDocumentPickerRootElement>event.target).nodeId;
break;
case 'media':
this.nodeId = (<UmbInputMediaElement>event.target).selectedIds.join('');
break;
case 'member':
default:
break;
}
@@ -103,11 +99,9 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
case 'content':
return this.#renderTypeContent();
case 'media':
return this.#renderTypeMedia();
case 'member':
return this.#renderTypeMember();
default:
return 'No type found';
return nothing;
}
}
@@ -117,18 +111,6 @@ export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElem
.nodeId=${this.nodeId}></umb-input-document-picker-root>`;
}
#renderTypeMedia() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => MediaTypes
return html`<umb-input-media @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-media>`;
}
#renderTypeMember() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => Members
return html`<umb-input-member @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-member>`;
}
static styles = [
css`
:host {

View File

@@ -21,7 +21,7 @@ const SORTER_CONFIG: UmbSorterConfig<UmbSwatchDetails> = {
return element.getAttribute('data-sort-entry-id') === model.value;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: UmbSwatchDetails) => {
return container.querySelector('data-sort-entry-id=[' + modelEntry.value + ']');
return container.querySelector('[data-sort-entry-id=' + modelEntry.value + ']');
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-multiple-color-picker-item-input',
@@ -35,21 +35,10 @@ const SORTER_CONFIG: UmbSorterConfig<UmbSwatchDetails> = {
export class UmbMultipleColorPickerInputElement extends FormControlMixin(UmbLitElement) {
#sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
performItemInsert: (args) => {
const frozenArray = [...this.items];
const indexToMove = frozenArray.findIndex((x) => x.value === args.item.value);
frozenArray.splice(indexToMove, 1);
frozenArray.splice(args.newIndex, 0, args.item);
this.items = frozenArray;
this.dispatchEvent(new UmbChangeEvent());
return true;
},
performItemRemove: (args) => {
return true;
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
this.requestUpdate('_items', oldValue);
},
});
@@ -150,7 +139,7 @@ export class UmbMultipleColorPickerInputElement extends FormControlMixin(UmbLitE
}
#onAdd() {
this._items = [...this._items, { value: '', label: '' }];
this.items = [...this._items, { value: '', label: '' }];
this.pristine = false;
this.dispatchEvent(new UmbChangeEvent());
this.#focusNewItem();
@@ -180,7 +169,7 @@ export class UmbMultipleColorPickerInputElement extends FormControlMixin(UmbLitE
#deleteItem(event: UmbDeleteEvent, itemIndex: number) {
event.stopPropagation();
this._items = this._items.filter((item, index) => index !== itemIndex);
this.items = this._items.filter((item, index) => index !== itemIndex);
this.pristine = false;
this.dispatchEvent(new UmbChangeEvent());
}

View File

@@ -16,7 +16,7 @@ const SORTER_CONFIG: UmbSorterConfig<MultipleTextStringValueItem> = {
return element.getAttribute('data-sort-entry-id') === model.value;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: MultipleTextStringValueItem) => {
return container.querySelector('data-sort-entry-id=[' + modelEntry.value + ']');
return container.querySelector('[data-sort-entry-id=' + modelEntry.value + ']');
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-input-multiple-text-string-item',
@@ -30,21 +30,10 @@ const SORTER_CONFIG: UmbSorterConfig<MultipleTextStringValueItem> = {
export class UmbInputMultipleTextStringElement extends FormControlMixin(UmbLitElement) {
#prevalueSorter = new UmbSorterController(this, {
...SORTER_CONFIG,
performItemInsert: (args) => {
const frozenArray = [...this.items];
const indexToMove = frozenArray.findIndex((x) => x.value === args.item.value);
frozenArray.splice(indexToMove, 1);
frozenArray.splice(args.newIndex, 0, args.item);
this.items = frozenArray;
this.dispatchEvent(new UmbChangeEvent());
return true;
},
performItemRemove: (args) => {
return true;
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
this.requestUpdate('_items', oldValue);
},
});

View File

@@ -39,12 +39,12 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
await this._dataTypeDetailRepository.byUnique(dataTypeUnique),
(dataType) => {
this._dataTypeData = dataType?.values;
this._propertyEditorUiAlias = dataType?.propertyEditorUiAlias || undefined;
this._propertyEditorUiAlias = dataType?.editorUiAlias || undefined;
// If there is no UI, we will look up the Property editor model to find the default UI alias:
if (!this._propertyEditorUiAlias && dataType?.propertyEditorAlias) {
//use 'dataType.propertyEditorAlias' to look up the extension in the registry:
if (!this._propertyEditorUiAlias && dataType?.editorAlias) {
//use 'dataType.editorAlias' to look up the extension in the registry:
this.observe(
umbExtensionsRegistry.getByTypeAndAlias('propertyEditorSchema', dataType.propertyEditorAlias),
umbExtensionsRegistry.getByTypeAndAlias('propertyEditorSchema', dataType.editorAlias),
(extension) => {
if (!extension) return;
this._propertyEditorUiAlias = extension?.meta.defaultPropertyEditorUiAlias;

View File

@@ -1,11 +1,8 @@
import { UMB_DATATYPE_WORKSPACE_MODAL } from '../../index.js';
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import {
UmbModalRouteRegistrationController,
UMB_DATA_TYPE_PICKER_FLOW_MODAL,
UMB_WORKSPACE_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UmbModalRouteRegistrationController, UMB_DATA_TYPE_PICKER_FLOW_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
// Note: Does only support picking a single data type. But this could be developed later into this same component. To follow other picker input components.
@@ -49,9 +46,7 @@ export class UmbInputDataTypeElement extends FormControlMixin(UmbLitElement) {
constructor() {
super();
this.#editDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL).onSetup(() => {
return { data: { entityType: 'data-type', preset: {} } };
});
this.#editDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_DATATYPE_WORKSPACE_MODAL);
new UmbModalRouteRegistrationController(this, UMB_DATA_TYPE_PICKER_FLOW_MODAL)
.onSetup(() => {

View File

@@ -28,8 +28,8 @@ export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) {
(dataType) => {
if (dataType) {
this.name = dataType.name ?? '';
this.propertyEditorUiAlias = dataType.propertyEditorUiAlias ?? '';
this.propertyEditorSchemaAlias = dataType.propertyEditorAlias ?? '';
this.propertyEditorUiAlias = dataType.editorUiAlias ?? '';
this.propertyEditorSchemaAlias = dataType.editorAlias ?? '';
}
},
'dataType',

View File

@@ -4,7 +4,6 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import {
UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL,
UMB_WORKSPACE_MODAL,
UmbDataTypePickerFlowModalData,
UmbDataTypePickerFlowModalValue,
UmbModalBaseElement,
@@ -13,6 +12,7 @@ import {
} from '@umbraco-cms/backoffice/modal';
import { ManifestPropertyEditorUi, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbEntityTreeItemModel } from '@umbraco-cms/backoffice/tree';
import { UMB_DATATYPE_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/data-type';
interface GroupedItems<T> {
[key: string]: Array<T>;
@@ -73,7 +73,7 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement<
this.requestUpdate('_dataTypePickerModalRouteBuilder');
});
this._createDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
this._createDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_DATATYPE_WORKSPACE_MODAL)
.addAdditionalPath(':uiAlias')
.onSetup((params) => {
return { data: { entityType: 'data-type', preset: { editorUiAlias: params.uiAlias } } };

View File

@@ -40,8 +40,8 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource<UmbDataT
unique: UmbId.new(),
parentUnique,
name: '',
propertyEditorAlias: undefined,
propertyEditorUiAlias: null,
editorAlias: undefined,
editorUiAlias: null,
values: [],
};
@@ -69,8 +69,8 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource<UmbDataT
unique: data.id,
parentUnique: data.parentId || null,
name: data.name,
propertyEditorAlias: data.editorAlias,
propertyEditorUiAlias: data.editorUiAlias || null,
editorAlias: data.editorAlias,
editorUiAlias: data.editorUiAlias || null,
values: data.values as Array<UmbDataTypePropertyModel>,
};
@@ -86,15 +86,15 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource<UmbDataT
async create(dataType: UmbDataTypeDetailModel) {
if (!dataType) throw new Error('Data Type is missing');
if (!dataType.unique) throw new Error('Data Type unique is missing');
if (!dataType.propertyEditorAlias) throw new Error('Property Editor Alias is missing');
if (!dataType.editorAlias) throw new Error('Property Editor Alias is missing');
// TODO: make data mapper to prevent errors
const requestBody: CreateDataTypeRequestModel = {
id: dataType.unique,
parentId: dataType.parentUnique,
name: dataType.name,
editorAlias: dataType.propertyEditorAlias,
editorUiAlias: dataType.propertyEditorUiAlias,
editorAlias: dataType.editorAlias,
editorUiAlias: dataType.editorUiAlias,
values: dataType.values,
};
@@ -121,13 +121,13 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource<UmbDataT
*/
async update(data: UmbDataTypeDetailModel) {
if (!data.unique) throw new Error('Unique is missing');
if (!data.propertyEditorAlias) throw new Error('Property Editor Alias is missing');
if (!data.editorAlias) throw new Error('Property Editor Alias is missing');
// TODO: make data mapper to prevent errors
const requestBody: DataTypeModelBaseModel = {
name: data.name,
editorAlias: data.propertyEditorAlias,
editorUiAlias: data.propertyEditorUiAlias,
editorAlias: data.editorAlias,
editorUiAlias: data.editorUiAlias,
values: data.values,
};

View File

@@ -21,9 +21,7 @@ export class UmbDataTypeDetailStore extends UmbDetailStoreBase<UmbDataTypeDetail
withPropertyEditorUiAlias(propertyEditorUiAlias: string) {
// TODO: Use a model for the data-type tree items: ^^Most likely it should be parsed to the UmbEntityTreeStore as a generic type.
return this._data.asObservablePart((items) =>
items.filter((item) => item.propertyEditorUiAlias === propertyEditorUiAlias),
);
return this._data.asObservablePart((items) => items.filter((item) => item.editorUiAlias === propertyEditorUiAlias));
}
}

View File

@@ -5,8 +5,8 @@ export interface UmbDataTypeDetailModel {
unique: string;
parentUnique: string | null;
name: string;
propertyEditorAlias: string | undefined;
propertyEditorUiAlias: string | null;
editorAlias: string | undefined;
editorUiAlias: string | null;
values: Array<UmbDataTypePropertyModel>;
}

View File

@@ -32,8 +32,8 @@ export class UmbDataTypeWorkspaceContext
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly unique = this.#data.asObservablePart((data) => data?.unique);
readonly propertyEditorUiAlias = this.#data.asObservablePart((data) => data?.propertyEditorUiAlias);
readonly propertyEditorSchemaAlias = this.#data.asObservablePart((data) => data?.propertyEditorAlias);
readonly propertyEditorUiAlias = this.#data.asObservablePart((data) => data?.editorUiAlias);
readonly propertyEditorSchemaAlias = this.#data.asObservablePart((data) => data?.editorAlias);
#properties = new UmbArrayState<PropertyEditorConfigProperty>([], (x) => x.alias);
readonly properties = this.#properties.asObservable();
@@ -231,10 +231,10 @@ export class UmbDataTypeWorkspaceContext
}
setPropertyEditorSchemaAlias(alias?: string) {
this.#data.update({ propertyEditorAlias: alias });
this.#data.update({ editorAlias: alias });
}
setPropertyEditorUiAlias(alias?: string) {
this.#data.update({ propertyEditorUiAlias: alias });
this.#data.update({ editorUiAlias: alias });
}
async propertyValueByAlias<ReturnType = unknown>(propertyAlias: string) {

View File

@@ -0,0 +1,14 @@
import { UmbDataTypeDetailModel } from '../types.js';
import { UmbModalToken, UmbWorkspaceData, UmbWorkspaceValue } from '@umbraco-cms/backoffice/modal';
export const UMB_DATATYPE_WORKSPACE_MODAL = new UmbModalToken<
UmbWorkspaceData<UmbDataTypeDetailModel>,
UmbWorkspaceValue
>('Umb.Modal.Workspace', {
modal: {
type: 'sidebar',
size: 'large',
},
data: { entityType: 'data-type', preset: {} },
// Recast the type, so the entityType data prop is not required:
}) as UmbModalToken<Omit<UmbWorkspaceData<UmbDataTypeDetailModel>, 'entityType'>, UmbWorkspaceValue>;

View File

@@ -1 +1,2 @@
export * from './data-type-workspace.context-token.js';
export * from './data-type-workspace.modal-token.js';

View File

@@ -41,11 +41,11 @@ export class UmbWorkspaceViewDataTypeInfoElement extends UmbLitElement implement
<div slot="editor">${this._dataType?.unique}</div>
</umb-property-layout>
<umb-property-layout label="Property Editor Alias">
<div slot="editor">${this._dataType?.propertyEditorAlias}</div>
<div slot="editor">${this._dataType?.editorAlias}</div>
</umb-property-layout>
<umb-property-layout label="Property Editor UI Alias">
<div slot="editor">${this._dataType?.propertyEditorUiAlias}</div>
<div slot="editor">${this._dataType?.editorUiAlias}</div>
</umb-property-layout>
</uui-box>
`;

View File

@@ -17,7 +17,6 @@ export * from './extension-registry/index.js';
export * from './id/index.js';
export * from './macro/index.js';
export * from './menu/index.js';
export * from './meta/index.js';
export * from './modal/index.js';
export * from './notification/index.js';
export * from './picker-input/index.js';

View File

@@ -1,7 +0,0 @@
import packageJson from '../../../../package.json';
export const umbMeta = {
name: 'Bellissima',
clientName: packageJson.name,
clientVersion: packageJson.version,
};

View File

@@ -1,10 +1,7 @@
import { CreateDataTypeRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
// TODO: Change model:
export interface UmbWorkspaceData {
export interface UmbWorkspaceData<DataModelType = unknown> {
entityType: string;
preset: Partial<CreateDataTypeRequestModel>;
preset: Partial<DataModelType>;
}
// TODO: It would be good with a WorkspaceValueBaseType, to avoid the hardcoded type for unique here:

View File

@@ -1,7 +1,7 @@
import { type UmbTreePickerSource, UmbInputTreePickerSourceElement } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { type UmbPropertyEditorConfigCollection, UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -25,7 +25,7 @@ export class UmbPropertyEditorUITreePickerSourcePickerElement extends UmbLitElem
dynamicRoot: target.dynamicRoot,
};
this.dispatchEvent(new CustomEvent('property-value-change'));
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {

View File

@@ -1,18 +1,22 @@
import { UmbInputDocumentTypeElement } from '@umbraco-cms/backoffice/document-type';
import { UmbInputMediaTypeElement } from '@umbraco-cms/backoffice/media-type';
import { UmbMemberTypeInputElement } from '@umbraco-cms/backoffice/member-type';
import { UmbInputMemberTypeElement } from '@umbraco-cms/backoffice/member-type';
import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
/**
* @element umb-property-editor-ui-tree-picker-source-type-picker
*/
@customElement('umb-property-editor-ui-tree-picker-source-type-picker')
export class UmbPropertyEditorUITreePickerSourceTypePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
export class UmbPropertyEditorUITreePickerSourceTypePickerElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
#datasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
@property({ type: Array })
@@ -42,7 +46,7 @@ export class UmbPropertyEditorUITreePickerSourceTypePickerElement extends UmbLit
// If we had a sourceType before, we can see this as a change and not the initial value,
// so let's reset the value, so we don't carry over content-types to the new source type.
if (this.#initialized && this.sourceType !== startNode.type) {
this.value = [];
this.#setValue([]);
}
this.sourceType = startNode.type;
@@ -59,19 +63,22 @@ export class UmbPropertyEditorUITreePickerSourceTypePickerElement extends UmbLit
#onChange(event: CustomEvent) {
switch (this.sourceType) {
case 'content':
this.value = (<UmbInputDocumentTypeElement>event.target).selectedIds;
this.#setValue((<UmbInputDocumentTypeElement>event.target).selectedIds);
break;
case 'media':
this.value = (<UmbInputMediaTypeElement>event.target).selectedIds;
this.#setValue((<UmbInputMediaTypeElement>event.target).selectedIds);
break;
case 'member':
this.value = (<UmbMemberTypeInputElement>event.target).selectedIds;
this.#setValue((<UmbInputMemberTypeElement>event.target).selectedIds);
break;
default:
break;
}
}
this.dispatchEvent(new CustomEvent('property-value-change'));
#setValue(value: string[]) {
this.value = value;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {
@@ -87,7 +94,7 @@ export class UmbPropertyEditorUITreePickerSourceTypePickerElement extends UmbLit
case 'member':
return this.#renderTypeMember();
default:
return 'No source type found';
return html`<p>No source type found</p>`;
}
}

View File

@@ -1,8 +1,8 @@
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { type UmbPropertyEditorConfigCollection, UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbInputTreeElement } from '@umbraco-cms/backoffice/tree';
import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
@@ -44,8 +44,8 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
this.startNodeId = startNode.id;
}
this.min = config?.getValueByAlias('minNumber') || 0;
this.max = config?.getValueByAlias('maxNumber') || 0;
this.min = Number(config?.getValueByAlias('minNumber')) || 0;
this.max = Number(config?.getValueByAlias('maxNumber')) || 0;
this.filter = config?.getValueByAlias('filter');
this.showOpenButton = config?.getValueByAlias('showOpenButton');
@@ -54,7 +54,7 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
#onChange(e: CustomEvent) {
this.value = (e.target as UmbInputTreeElement).value as string;
this.dispatchEvent(new CustomEvent('property-value-change'));
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {

View File

@@ -61,27 +61,23 @@ function destroyPreventEvent(element: Element) {
element.removeAttribute('draggable');
}
type INTERNAL_UmbSorterConfig<T> = {
compareElementToModel: (el: HTMLElement, modelEntry: T) => boolean;
querySelectModelToElement: (container: HTMLElement, modelEntry: T) => HTMLElement | null;
type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
compareElementToModel: (el: ElementType, modelEntry: T) => boolean;
querySelectModelToElement: (container: HTMLElement, modelEntry: T) => ElementType | null;
identifier: string;
itemSelector: string;
disabledItemSelector?: string;
containerSelector: string;
ignorerSelector: string;
placeholderClass: string;
placeholderClass?: string;
placeholderAttr?: string;
draggableSelector?: string;
boundarySelector?: string;
dataTransferResolver?: (dataTransfer: DataTransfer | null, currentItem: T) => void;
onStart?: (argument: { item: T; element: HTMLElement }) => void;
onChange?: (argument: { item: T; element: HTMLElement }) => void;
onContainerChange?: (argument: { item: T; element: HTMLElement }) => void;
onEnd?: (argument: { item: T; element: HTMLElement }) => void;
onSync?: (argument: {
item: T;
fromController: UmbSorterController<T>;
toController: UmbSorterController<T>;
}) => void;
onStart?: (argument: { item: T; element: ElementType }) => void;
onChange?: (argument: { item: T; model: Array<T> }) => void;
onContainerChange?: (argument: { item: T; element: ElementType }) => void;
onEnd?: (argument: { item: T; element: ElementType }) => void;
itemHasNestedContainersResolver?: (element: HTMLElement) => boolean;
onDisallowed?: () => void;
onAllowed?: () => void;
@@ -90,23 +86,24 @@ type INTERNAL_UmbSorterConfig<T> = {
containerElement: Element;
containerRect: DOMRect;
item: T;
element: HTMLElement;
element: ElementType;
elementRect: DOMRect;
relatedElement: HTMLElement;
relatedElement: ElementType;
relatedRect: DOMRect;
placeholderIsInThisRow: boolean;
horizontalPlaceAfter: boolean;
}) => void;
performItemMove?: (argument: { item: T; newIndex: number; oldIndex: number }) => Promise<boolean> | boolean;
performItemInsert?: (argument: { item: T; newIndex: number }) => Promise<boolean> | boolean;
performItemRemove?: (argument: { item: T }) => Promise<boolean> | boolean;
};
// External type with some properties optional, as they have defaults:
export type UmbSorterConfig<T> = Omit<
INTERNAL_UmbSorterConfig<T>,
'placeholderClass' | 'ignorerSelector' | 'containerSelector'
export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> = Omit<
INTERNAL_UmbSorterConfig<T, ElementType>,
'ignorerSelector' | 'containerSelector'
> &
Partial<Pick<INTERNAL_UmbSorterConfig<T>, 'placeholderClass' | 'ignorerSelector' | 'containerSelector'>>;
Partial<Pick<INTERNAL_UmbSorterConfig<T, ElementType>, 'ignorerSelector' | 'containerSelector'>>;
/**
* @export
@@ -114,55 +111,58 @@ export type UmbSorterConfig<T> = Omit<
* @implements {UmbControllerInterface}
* @description This controller can make user able to sort items.
*/
export class UmbSorterController<T> implements UmbController {
export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElement> implements UmbController {
#host;
#config: INTERNAL_UmbSorterConfig<T>;
#config: INTERNAL_UmbSorterConfig<T, ElementType>;
#observer;
#model: Array<T> = [];
#rqaId?: number;
#containerElement!: HTMLElement;
#currentContainerVM = this;
#currentContainerCtrl: UmbSorterController<T, ElementType> = this;
#currentContainerElement: Element | null = null;
#useContainerShadowRoot?: boolean;
#scrollElement?: Element | null;
#currentElement?: HTMLElement;
#currentElement?: ElementType;
#currentDragElement?: Element;
#currentDragRect?: DOMRect;
#currentItem?: T | null;
#currentItem?: T;
#currentIndex?: number;
#dragX = 0;
#dragY = 0;
private _lastIndicationContainerVM: UmbSorterController<T> | null = null;
#lastIndicationContainerCtrl: UmbSorterController<T, ElementType> | null = null;
public get controllerAlias() {
return this.#config.identifier;
}
constructor(host: UmbControllerHostElement, config: UmbSorterConfig<T>) {
constructor(host: UmbControllerHostElement, config: UmbSorterConfig<T, ElementType>) {
this.#host = host;
// Set defaults:
config.ignorerSelector ??= 'a, img, iframe';
config.placeholderClass ??= '--umb-sorter-placeholder';
if (!config.placeholderClass && !config.placeholderAttr) {
config.placeholderAttr = 'drag-placeholder';
}
this.#config = config as INTERNAL_UmbSorterConfig<T>;
this.#config = config as INTERNAL_UmbSorterConfig<T, ElementType>;
host.addController(this);
//this.#currentContainerElement = host;
this.#observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((addedNode) => {
if ((addedNode as HTMLElement).matches && (addedNode as HTMLElement).matches(this.#config.itemSelector)) {
this.setupItem(addedNode as HTMLElement);
this.setupItem(addedNode as ElementType);
}
});
mutation.removedNodes.forEach((removedNode) => {
if ((removedNode as HTMLElement).matches && (removedNode as HTMLElement).matches(this.#config.itemSelector)) {
this.destroyItem(removedNode as HTMLElement);
this.destroyItem(removedNode as ElementType);
}
});
});
@@ -185,7 +185,9 @@ export class UmbSorterController<T> implements UmbController {
? this.#host.shadowRoot!.querySelector(this.#config.containerSelector)
: this.#host) ?? this.#host;
if (this.#currentContainerElement === this.#containerElement) {
this.#useContainerShadowRoot = this.#containerElement === this.#host;
if (!this.#currentContainerElement || this.#currentContainerElement === this.#containerElement) {
this.#currentContainerElement = containerEl;
}
this.#containerElement = containerEl as HTMLElement;
@@ -198,10 +200,13 @@ export class UmbSorterController<T> implements UmbController {
// TODO: Clean up??
this.#observer.disconnect();
const containerElement = this.#containerElement.shadowRoot ?? this.#containerElement;
// Only look at the shadowRoot if the containerElement is host.
const containerElement = this.#useContainerShadowRoot
? this.#containerElement.shadowRoot ?? this.#containerElement
: this.#containerElement;
containerElement.querySelectorAll(this.#config.itemSelector).forEach((child) => {
if (child.matches && child.matches(this.#config.itemSelector)) {
this.setupItem(child as HTMLElement);
this.setupItem(child as ElementType);
}
});
this.#observer.observe(containerElement, {
@@ -219,14 +224,22 @@ export class UmbSorterController<T> implements UmbController {
}
}
setupItem(element: HTMLElement) {
setupItem(element: ElementType) {
if (this.#config.ignorerSelector) {
setupIgnorerElements(element, this.#config.ignorerSelector);
}
if (!this.#config.disabledItemSelector || !element.matches(this.#config.disabledItemSelector)) {
element.draggable = true;
element.addEventListener('dragstart', this.handleDragStart);
element.addEventListener('dragstart', this.#handleDragStart);
}
// If we have a currentItem and the element matches, we should set the currentElement to this element.
if (this.#currentItem && this.#config.compareElementToModel(element, this.#currentItem)) {
if (this.#currentElement !== element) {
console.log('THIS ACTUALLY HAPPENED... NOTICE THIS!');
this.#setCurrentElement(element);
}
}
}
@@ -235,27 +248,28 @@ export class UmbSorterController<T> implements UmbController {
destroyIgnorerElements(element, this.#config.ignorerSelector);
}
element.removeEventListener('dragstart', this.handleDragStart);
element.removeEventListener('dragstart', this.#handleDragStart);
}
handleDragStart = (event: DragEvent) => {
if (this.#currentElement) {
this.handleDragEnd();
#setupPlaceholderStyle() {
if (this.#config.placeholderClass) {
this.#currentElement?.classList.add(this.#config.placeholderClass);
}
if (this.#config.placeholderAttr) {
this.#currentElement?.setAttribute(this.#config.placeholderAttr, '');
}
}
#removePlaceholderStyle() {
if (this.#config.placeholderClass) {
this.#currentElement?.classList.remove(this.#config.placeholderClass);
}
if (this.#config.placeholderAttr) {
this.#currentElement?.removeAttribute(this.#config.placeholderAttr);
}
}
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data.
event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped.
}
if (!this.#scrollElement) {
this.#scrollElement = getParentScrollElement(this.#containerElement, true);
}
const element = (event.target as HTMLElement).closest(this.#config.itemSelector);
if (!element) return;
#setCurrentElement(element: ElementType) {
this.#currentElement = element;
this.#currentDragElement = this.#config.draggableSelector
? element.querySelector(this.#config.draggableSelector) ?? undefined
@@ -270,90 +284,88 @@ export class UmbSorterController<T> implements UmbController {
return;
}
this.#currentElement = element as HTMLElement;
this.#currentDragRect = this.#currentDragElement.getBoundingClientRect();
this.#currentItem = this.getItemOfElement(this.#currentElement);
this.#setupPlaceholderStyle();
}
#handleDragStart = (event: DragEvent) => {
const element = (event.target as HTMLElement).closest(this.#config.itemSelector);
if (!element) return;
if (this.#currentElement && this.#currentElement !== element) {
this.#handleDragEnd();
}
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data.
event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped.
}
if (!this.#scrollElement) {
this.#scrollElement = getParentScrollElement(this.#containerElement, true);
}
this.#setCurrentElement(element as ElementType);
this.#currentDragRect = this.#currentDragElement?.getBoundingClientRect();
this.#currentItem = this.getItemOfElement(this.#currentElement!);
if (!this.#currentItem) {
console.error('Could not find item related to this element.');
return;
}
this.#currentElement.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
// Get the current index of the item:
this.#currentIndex = this.#model.indexOf(this.#currentItem);
this.#currentElement!.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
if (this.#config.dataTransferResolver) {
this.#config.dataTransferResolver(event.dataTransfer, this.#currentItem);
}
if (this.#config.onStart) {
this.#config.onStart({ item: this.#currentItem!, element: this.#currentElement });
this.#config.onStart({ item: this.#currentItem, element: this.#currentElement! });
}
window.addEventListener('dragover', this.handleDragMove);
window.addEventListener('dragend', this.handleDragEnd);
window.addEventListener('dragover', this.#handleDragMove);
window.addEventListener('dragend', this.#handleDragEnd);
// We must wait one frame before changing the look of the block.
this.#rqaId = requestAnimationFrame(() => {
// It should be okay to use the same rqaId, as the move does not or is okay not to happen on first frame/drag-move.
// It should be okay to use the same rqaId, as the move does not, or is okay not, to happen on first frame/drag-move.
this.#rqaId = undefined;
if (this.#currentElement) {
this.#currentElement.style.transform = '';
this.#currentElement.classList.add(this.#config.placeholderClass);
this.#setupPlaceholderStyle();
}
});
};
handleDragEnd = async () => {
#handleDragEnd = async () => {
window.removeEventListener('dragover', this.#handleDragMove);
window.removeEventListener('dragend', this.#handleDragEnd);
if (!this.#currentElement || !this.#currentItem) {
return;
}
window.removeEventListener('dragover', this.handleDragMove);
window.removeEventListener('dragend', this.handleDragEnd);
this.#currentElement.style.transform = '';
this.#currentElement.classList.remove(this.#config.placeholderClass);
this.#removePlaceholderStyle();
this.stopAutoScroll();
this.#stopAutoScroll();
this.removeAllowIndication();
if ((await this.#currentContainerVM.sync(this.#currentElement, this)) === false) {
// Sync could not succeed, might be because item is not allowed here.
this.#currentContainerVM = this;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
element: this.#currentElement,
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
// Lets move the Element back to where it came from:
const movingItemIndex = this.#model.indexOf(this.#currentItem);
if (movingItemIndex < this.#model.length - 1) {
const afterItem = this.#model[movingItemIndex + 1];
const afterEl = this.#config.querySelectModelToElement(this.#containerElement, afterItem);
if (afterEl) {
this.#containerElement.insertBefore(this.#currentElement, afterEl);
} else {
this.#containerElement.appendChild(this.#currentElement);
}
} else {
this.#containerElement.appendChild(this.#currentElement);
}
}
if (this.#config.onEnd) {
this.#config.onEnd({ item: this.#currentItem, element: this.#currentElement });
}
if (this.#rqaId) {
cancelAnimationFrame(this.#rqaId);
this.#rqaId = undefined;
}
this.#currentContainerElement = this.#containerElement;
this.#currentContainerVM = this;
this.#currentContainerCtrl = this;
this.#rqaId = undefined;
this.#currentItem = undefined;
this.#currentElement = undefined;
this.#currentDragElement = undefined;
@@ -362,7 +374,7 @@ export class UmbSorterController<T> implements UmbController {
this.#dragY = 0;
};
handleDragMove = (event: DragEvent) => {
#handleDragMove = (event: DragEvent) => {
if (!this.#currentElement) {
return;
}
@@ -386,13 +398,13 @@ export class UmbSorterController<T> implements UmbController {
const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, this.#currentDragRect);
if (!insideCurrentRect) {
if (this.#rqaId === undefined) {
this.#rqaId = requestAnimationFrame(this.moveCurrentElement);
this.#rqaId = requestAnimationFrame(this.#updateDragMove);
}
}
}
};
moveCurrentElement = () => {
#updateDragMove = () => {
this.#rqaId = undefined;
if (!this.#currentElement || !this.#currentContainerElement || !this.#currentItem) {
return;
@@ -404,7 +416,9 @@ export class UmbSorterController<T> implements UmbController {
return;
}
// If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement.
let toBeCurrentContainerCtrl: UmbSorterController<T, ElementType> | undefined = undefined;
// If we have a boundarySelector, try it. If we didn't get anything fall back to currentContainerElement:
const currentBoundaryElement =
(this.#config.boundarySelector
? this.#currentContainerElement.closest(this.#config.boundarySelector)
@@ -412,24 +426,25 @@ export class UmbSorterController<T> implements UmbController {
const currentBoundaryRect = currentBoundaryElement.getBoundingClientRect();
const currentContainerHasItems = this.#currentContainerVM.hasOtherItemsThan(this.#currentItem!);
const currentContainerHasItems = this.#currentContainerCtrl.hasOtherItemsThan(this.#currentItem);
// if empty we will be move likely to accept an item (add 20px to the bounding box)
// If we have items we must be 10 within the container to accept the move.
// If we have items we must be 10px within the container to accept the move.
const offsetEdge = currentContainerHasItems ? -10 : 20;
if (!isWithinRect(this.#dragX, this.#dragY, currentBoundaryRect, offsetEdge)) {
// we are outside the current container boundary, so lets see if there is a parent we can move.
// we are outside the current container boundary, so lets see if there is a parent we can move to.
const parentNode = this.#currentContainerElement.parentNode;
if (parentNode) {
if (parentNode && this.#config.containerSelector) {
// TODO: support multiple parent shadowDOMs?
const parentContainer = this.#config.containerSelector
? (parentNode as HTMLElement).closest(this.#config.containerSelector)
: null;
const parentContainer = (parentNode as ShadowRoot).host
? (parentNode as ShadowRoot).host.closest(this.#config.containerSelector)
: (parentNode as HTMLElement).closest(this.#config.containerSelector);
if (parentContainer) {
const parentContainerVM = (parentContainer as any)['__umbBlockGridSorterController']();
if (parentContainerVM.unique === this.controllerAlias) {
const parentContainerCtrl = (parentContainer as any)['__umbBlockGridSorterController']();
if (parentContainerCtrl.unique === this.controllerAlias) {
this.#currentContainerElement = parentContainer as Element;
this.#currentContainerVM = parentContainerVM;
toBeCurrentContainerCtrl = parentContainerCtrl;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
@@ -442,12 +457,12 @@ export class UmbSorterController<T> implements UmbController {
}
}
const containerElement = this.#useContainerShadowRoot
? this.#currentContainerElement.shadowRoot ?? this.#currentContainerElement
: this.#currentContainerElement;
// We want to retrieve the children of the container, every time to ensure we got the right order and index
const orderedContainerElements = Array.from(
this.#currentContainerElement.shadowRoot
? this.#currentContainerElement.shadowRoot.querySelectorAll(this.#config.itemSelector)
: this.#currentContainerElement.querySelectorAll(this.#config.itemSelector),
);
const orderedContainerElements = Array.from(containerElement.querySelectorAll(this.#config.itemSelector));
const currentContainerRect = this.#currentContainerElement.getBoundingClientRect();
@@ -515,10 +530,10 @@ export class UmbSorterController<T> implements UmbController {
// gather elements on the same row.
const subOffsetEdge = subContainerHasItems ? -10 : 20;
if (isWithinRect(this.#dragX, this.#dragY, subBoundaryRect, subOffsetEdge)) {
const subVm = (subLayoutEl as any)['__umbBlockGridSorterController']();
if (subVm.unique === this.controllerAlias) {
const subCtrl = (subLayoutEl as any)['__umbBlockGridSorterController']();
if (subCtrl.unique === this.controllerAlias) {
this.#currentContainerElement = subLayoutEl as HTMLElement;
this.#currentContainerVM = subVm;
toBeCurrentContainerCtrl = subCtrl;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
@@ -526,7 +541,7 @@ export class UmbSorterController<T> implements UmbController {
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
this.moveCurrentElement();
this.#updateDragMove();
return;
}
}
@@ -534,7 +549,9 @@ export class UmbSorterController<T> implements UmbController {
}
// Indication if drop is good:
if (this.updateAllowIndication(this.#currentContainerVM, this.#currentItem) === false) {
if (
this.updateAllowIndication(toBeCurrentContainerCtrl ?? this.#currentContainerCtrl, this.#currentItem) === false
) {
return;
}
@@ -581,162 +598,154 @@ export class UmbSorterController<T> implements UmbController {
}
const foundElIndex = orderedContainerElements.indexOf(foundEl);
const placeAt = placeAfter ? foundElIndex + 1 : foundElIndex;
this.move(orderedContainerElements, placeAt);
const newIndex = placeAfter ? foundElIndex + 1 : foundElIndex;
this.#moveElementTo(toBeCurrentContainerCtrl, newIndex);
return;
}
// We skipped the above part cause we are above or below container:
// Indication if drop is good:
if (this.updateAllowIndication(this.#currentContainerVM, this.#currentItem) === false) {
if (
this.updateAllowIndication(toBeCurrentContainerCtrl ?? this.#currentContainerCtrl, this.#currentItem) === false
) {
return;
}
if (this.#dragY < currentContainerRect.top) {
this.move(orderedContainerElements, 0);
this.#moveElementTo(toBeCurrentContainerCtrl, 0);
} else if (this.#dragY > currentContainerRect.bottom) {
this.move(orderedContainerElements, -1);
this.#moveElementTo(toBeCurrentContainerCtrl, -1);
}
};
move(orderedContainerElements: Array<Element>, newElIndex: number) {
if (!this.#currentElement || !this.#currentItem || !this.#currentContainerElement) return;
newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex;
const containerElement = this.#currentContainerElement.shadowRoot ?? this.#currentContainerElement;
const placeBeforeElement = orderedContainerElements[newElIndex];
if (placeBeforeElement) {
// We do not need to move this, if the element to be placed before is it self.
if (placeBeforeElement !== this.#currentElement) {
containerElement.insertBefore(this.#currentElement, placeBeforeElement);
}
} else {
containerElement.appendChild(this.#currentElement);
async #moveElementTo(containerCtrl: UmbSorterController<T, ElementType> | undefined, newIndex: number) {
if (!this.#currentElement) {
return;
}
if (this.#config.onChange) {
this.#config.onChange({
element: this.#currentElement,
item: this.#currentItem,
//ownerVM: this.#currentContainerVM.ownerVM
});
containerCtrl ??= this as UmbSorterController<T, ElementType>;
// If same container and same index, do nothing:
if (this.#currentContainerCtrl === containerCtrl && this.#currentIndex === newIndex) return;
if (await containerCtrl.moveItemInModel(newIndex, this.#currentElement, this.#currentContainerCtrl)) {
this.#currentContainerCtrl = containerCtrl;
this.#currentIndex = newIndex;
}
}
/** Management methods: */
public getItemOfElement(element: HTMLElement) {
public getItemOfElement(element: ElementType) {
if (!element) {
return null;
return undefined;
}
return this.#model.find((entry: T) => this.#config.compareElementToModel(element, entry));
}
public async removeItem(item: T) {
if (!item) {
return null;
return false;
}
if (this.#config.performItemRemove) {
return await this.#config.performItemRemove({ item });
return (await this.#config.performItemRemove({ item })) ?? false;
} else {
const oldIndex = this.#model.indexOf(item);
if (oldIndex !== -1) {
return this.#model.splice(oldIndex, 1)[0];
const newModel = [...this.#model];
newModel.splice(oldIndex, 1);
this.#model = newModel;
this.#config.onChange?.({ model: newModel, item });
return true;
}
}
return null;
return false;
}
hasOtherItemsThan(item: T) {
public hasOtherItemsThan(item: T) {
return this.#model.filter((x) => x !== item).length > 0;
}
public async sync(element: HTMLElement, fromVm: UmbSorterController<T>) {
const movingItem = fromVm.getItemOfElement(element);
if (!movingItem) {
public async moveItemInModel(newIndex: number, element: ElementType, fromCtrl: UmbSorterController<T, ElementType>) {
const item = fromCtrl.getItemOfElement(element);
if (!item) {
console.error('Could not find item of sync item');
return false;
}
if (this.notifyRequestDrop({ item: movingItem }) === false) {
return false;
}
if (fromVm.removeItem(movingItem) === null) {
console.error('Sync could not remove item');
if (this.notifyRequestDrop({ item }) === false) {
return false;
}
/** Find next element, to then find the index of that element in items-data, to use as a safe reference to where the item will go in our items-data.
* This enables the container to contain various other elements and as well having these elements change while sorting is occurring.
*/
const localMove = fromCtrl === this;
// find next valid element (This assumes the next element in DOM is presented in items-data, aka. only moving one item between each sync)
let nextEl: Element | null = null;
let loopEl: Element | null = element;
while ((loopEl = loopEl?.nextElementSibling)) {
if (loopEl.matches && loopEl.matches(this.#config.itemSelector)) {
nextEl = loopEl;
break;
}
}
if (localMove) {
// Local move:
let newIndex = this.#model.length;
// TODO: Maybe this should be replaceable/configurable:
const oldIndex = this.#model.indexOf(item);
const movingItemIndex = this.#model.indexOf(movingItem);
if (movingItemIndex !== -1 && movingItemIndex <= movingItemIndex) {
newIndex--;
}
if (nextEl) {
// We had a reference element, we want to get the index of it.
// This is might a problem if a item is being moved forward? (was also like this in the AngularJS version...)
newIndex = this.#model.findIndex((entry) => this.#config.compareElementToModel(nextEl! as HTMLElement, entry));
}
if (this.#config.performItemInsert) {
const result = await this.#config.performItemInsert({ item: movingItem, newIndex });
if (this.#config.performItemMove) {
const result = await this.#config.performItemMove({ item, newIndex, oldIndex });
if (result === false) {
return false;
}
} else {
this.#model.splice(newIndex, 0, movingItem);
const newModel = [...this.#model];
newModel.splice(oldIndex, 1);
if (oldIndex <= newIndex) {
newIndex--;
}
newModel.splice(newIndex, 0, item);
this.#model = newModel;
this.#config.onChange?.({ model: newModel, item });
}
} else {
// Not a local move:
if ((await fromCtrl.removeItem(item)) !== true) {
console.error('Sync could not remove item');
return false;
}
const eventData = { item: movingItem, fromController: fromVm, toController: this };
if (fromVm !== this) {
fromVm.notifySync(eventData);
if (this.#config.performItemInsert) {
const result = await this.#config.performItemInsert({ item, newIndex });
if (result === false) {
return false;
}
} else {
const newModel = [...this.#model];
newModel.splice(newIndex, 0, item);
this.#model = newModel;
this.#config.onChange?.({ model: newModel, item });
}
}
this.notifySync(eventData);
return true;
}
updateAllowIndication(contextVM: UmbSorterController<T>, item: T) {
updateAllowIndication(controller: UmbSorterController<T, ElementType>, item: T) {
// Remove old indication:
if (this._lastIndicationContainerVM !== null && this._lastIndicationContainerVM !== contextVM) {
this._lastIndicationContainerVM.notifyAllowed();
if (this.#lastIndicationContainerCtrl !== null && this.#lastIndicationContainerCtrl !== controller) {
this.#lastIndicationContainerCtrl.notifyAllowed();
}
this._lastIndicationContainerVM = contextVM;
this.#lastIndicationContainerCtrl = controller;
if (contextVM.notifyRequestDrop({ item: item }) === true) {
contextVM.notifyAllowed();
if (controller.notifyRequestDrop({ item: item }) === true) {
controller.notifyAllowed();
return true;
}
contextVM.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
controller.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
return false;
}
removeAllowIndication() {
// Remove old indication:
if (this._lastIndicationContainerVM !== null) {
this._lastIndicationContainerVM.notifyAllowed();
if (this.#lastIndicationContainerCtrl !== null) {
this.#lastIndicationContainerCtrl.notifyAllowed();
}
this._lastIndicationContainerVM = null;
this.#lastIndicationContainerCtrl = null;
}
// TODO: Move auto scroll into its own class?
@@ -786,24 +795,19 @@ export class UmbSorterController<T> implements UmbController {
? -1
: 0;
this.#autoScrollRAF = requestAnimationFrame(this._performAutoScroll);
this.#autoScrollRAF = requestAnimationFrame(this.#performAutoScroll);
}
}
private _performAutoScroll = () => {
#performAutoScroll = () => {
this.#autoScrollEl!.scrollLeft += this.autoScrollX * autoScrollSpeed;
this.#autoScrollEl!.scrollTop += this.autoScrollY * autoScrollSpeed;
this.#autoScrollRAF = requestAnimationFrame(this._performAutoScroll);
this.#autoScrollRAF = requestAnimationFrame(this.#performAutoScroll);
};
private stopAutoScroll() {
#stopAutoScroll() {
cancelAnimationFrame(this.#autoScrollRAF!);
this.#autoScrollRAF = null;
}
public notifySync(data: any) {
if (this.#config.onSync) {
this.#config.onSync(data);
}
}
public notifyDisallowed() {
if (this.#config.onDisallowed) {
this.#config.onDisallowed();
@@ -824,10 +828,10 @@ export class UmbSorterController<T> implements UmbController {
destroy() {
// Do something when host element is destroyed.
if (this.#currentElement) {
this.handleDragEnd();
this.#handleDragEnd();
}
this._lastIndicationContainerVM = null;
this.#lastIndicationContainerCtrl = null;
// TODO: Clean up items??
this.#observer.disconnect();

View File

@@ -51,20 +51,20 @@ The configuration has a lot of optional options, but the required ones are:
- itemSelector
- containerSelector
It can be set up as following:
It can be set up as follows:
```typescript
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
type MySortEntryType = {...}
const awesomeModel: Array<MySortEntryType> = [...]
type MySortEntryType = {...};
const awesomeModel: Array<MySortEntryType> = [...];
const MY_SORTER_CONFIG: UmbSorterConfig<MySortEntryType> = {
compareElementToModel: (element: HTMLElement, model: MySortEntryType) => {
return element.getAttribute('data-sort-entry-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: MySortEntryType) => {
return container.querySelector('data-sort-entry-id=[' + modelEntry.id + ']');
return container.querySelector('[data-sort-entry-id=' + modelEntry.id + ']');
},
identifier: 'test-sorter',
itemSelector: 'li',
@@ -101,7 +101,13 @@ const MY_SORTER_CONFIG: UmbSorterConfig<MySortEntryType> = {...}
export class MyElement extends UmbElementMixin(LitElement) {
#sorter = new UmbSorterController(this, MY_SORTER_CONFIG);
#sorter = new UmbSorterController(this, {...MY_SORTER_CONFIG,
onChange: ({ model }) => {
const oldValue = this.awesomeModel;
this.awesomeModel = model;
this.requestUpdate('awesomeModel', oldValue);
},
});
constructor() {
this.#sorter.setModel(awesomeModel);
@@ -176,11 +182,13 @@ export class MyElement extends UmbElementMixin(LitElement) {
#sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
performItemInsert: ({ item, newIndex }) => {
console.log(item, newIndex);
// Perform some logic here to calculate the new sortOrder & save it.
// Insert logic that updates the model, so the item gets the new index in the model.
return true;
},
performItemRemove: () => {
// Insert logic that updates the model, so the item gets removed from the model.
return true;
},
performItemRemove: () => true,
});
}
```

View File

@@ -12,7 +12,7 @@ const SORTER_CONFIG: UmbSorterConfig<SortEntryType> = {
return element.getAttribute('data-sort-entry-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: SortEntryType) => {
return container.querySelector('data-sort-entry-id=[' + modelEntry.id + ']');
return container.querySelector('[data-sort-entry-id=' + modelEntry.id + ']');
},
identifier: 'test-sorter',
itemSelector: 'li',
@@ -40,6 +40,9 @@ export default class UmbTestSorterControllerElement extends UmbLitElement {
@state()
private vertical = true;
@state()
private _items: Array<SortEntryType> = [...model];
constructor() {
super();
this.sorter = new UmbSorterController(this, {
@@ -47,6 +50,11 @@ export default class UmbTestSorterControllerElement extends UmbLitElement {
resolveVerticalDirection: () => {
this.vertical ? true : false;
},
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
this.requestUpdate('_items', oldValue);
},
});
this.sorter.setModel(model);
}
@@ -61,7 +69,7 @@ export default class UmbTestSorterControllerElement extends UmbLitElement {
Horizontal/Vertical
</uui-button>
<ul class="${this.vertical ? 'vertical' : 'horizontal'}">
${model.map(
${this._items.map(
(entry) =>
html`<li class="item" data-sort-entry-id="${entry.id}">
<span><uui-icon name="icon-wand"></uui-icon>${entry.value}</span>

View File

@@ -2,6 +2,8 @@ import { css, html, customElement, property } from '@umbraco-cms/backoffice/exte
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
import { UmbInputMemberElement } from '@umbraco-cms/backoffice/member';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
@@ -64,7 +66,20 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
selectedIds: Array<string> = [];
#onChange(event: CustomEvent) {
switch (this._type) {
case 'content':
this.value = (event.target as UmbInputDocumentElement).selectedIds.join(',');
break;
case 'media':
this.value = (event.target as UmbInputMediaElement).selectedIds.join(',');
break;
case 'member':
this.value = (event.target as UmbInputMemberElement).selectedIds.join(',');
break;
default:
break;
}
this.dispatchEvent(new UmbChangeEvent());
}
@@ -75,6 +90,17 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
render() {
switch (this._type) {
case 'content':
return this.#renderContentPicker();
case 'media':
return this.#renderMediaPicker();
case 'member':
return this.#renderMemberPicker();
default:
return html`<p>Type could not be found.</p>`;
}
}
#renderContentPicker() {
return html`<umb-input-document
.selectedIds=${this.selectedIds}
.startNodeId=${this.startNodeId}
@@ -84,17 +110,20 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}></umb-input-document>`;
case 'media':
}
#renderMediaPicker() {
return html`<umb-input-media
.selectedIds=${this.selectedIds}
.startNodeId=${this.startNodeId}
.filter=${this.filter}
.min=${this.min}
.max=${this.max}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}></umb-input-media>`;
case 'member':
}
#renderMemberPicker() {
return html`<umb-input-member
.selectedIds=${this.selectedIds}
.filter=${this.filter}
@@ -102,11 +131,7 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
.max=${this.max}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
@change=${this.#onChange}>
</umb-input-member>`;
default:
return html`Type could not be found`;
}
@change=${this.#onChange}></umb-input-member>`;
}
static styles = [

View File

@@ -1,5 +1,14 @@
import { UmbDocumentTypePickerContext } from './input-document-type.context.js';
import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import {
css,
html,
customElement,
property,
state,
ifDefined,
repeat,
nothing,
} from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
@@ -95,6 +104,10 @@ export class UmbInputDocumentTypeElement extends FormControlMixin(UmbLitElement)
.observeRouteBuilder((routeBuilder) => {
this._editDocumentTypePath = routeBuilder({});
});
}
connectedCallback() {
super.connectedCallback();
this.addValidator(
'rangeUnderflow',
@@ -123,26 +136,47 @@ export class UmbInputDocumentTypeElement extends FormControlMixin(UmbLitElement)
pickableFilter: (x) => x.isElement,
});
} else {
this.#pickerContext.openPicker({ hideTreeRoot: true });
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
}
render() {
return html` <uui-ref-list>${this._items?.map((item) => this._renderItem(item))}</uui-ref-list>
${this.#renderAddButton()}`;
return html` ${this.#renderItems()} ${this.#renderAddButton()} `;
}
#renderItems() {
if (!this._items) return nothing;
return html`
<uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this.#renderItem(item),
)}</uui-ref-list
>
`;
}
#renderAddButton() {
if (this.max === 1 && this.selectedIds.length === 1) return nothing;
return html`<uui-button id="add-button" look="placeholder" @click=${this.#openPicker} label="open">
Add
</uui-button>`;
if (this.max > 0 && this.selectedIds.length >= this.max) return nothing;
return html`
<uui-button
id="add-button"
look="placeholder"
@click=${this.#openPicker}
label="${this.localize.term('general_choose')}"
>${this.localize.term('general_choose')}</uui-button
>
`;
}
private _renderItem(item: DocumentTypeItemResponseModel) {
#renderItem(item: DocumentTypeItemResponseModel) {
if (!item.id) return;
return html`
<uui-ref-node-document-type name=${ifDefined(item.name)}>
${this.#renderIcon(item)}
<uui-action-bar slot="actions">
<uui-button
compact
@@ -161,6 +195,11 @@ export class UmbInputDocumentTypeElement extends FormControlMixin(UmbLitElement)
`;
}
#renderIcon(item: DocumentTypeItemResponseModel) {
if (!item.icon) return;
return html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`;
}
static styles = [
css`
#add-button {

View File

@@ -18,7 +18,7 @@ const SORTER_CONFIG: UmbSorterConfig<DocumentTypePropertyTypeResponseModel> = {
return element.getAttribute('data-umb-property-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: DocumentTypePropertyTypeResponseModel) => {
return container.querySelector('data-umb-property-id=[' + modelEntry.id + ']');
return container.querySelector('[data-umb-property-id=' + modelEntry.id + ']');
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',
@@ -45,6 +45,19 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
performItemRemove: (args) => {
return this._propertyStructureHelper.removeProperty(args.item.id!);
},
performItemMove: (args) => {
this._propertyStructureHelper.removeProperty(args.item.id!);
let sortOrder = 0;
if (this._propertyStructure.length > 0) {
if (args.newIndex === 0) {
sortOrder = (this._propertyStructure[0].sortOrder ?? 0) - 1;
} else {
sortOrder =
(this._propertyStructure[Math.min(args.newIndex, this._propertyStructure.length - 1)].sortOrder ?? 0) + 1;
}
}
return this._propertyStructureHelper.insertProperty(args.item, sortOrder);
},
});
private _containerId: string | undefined;
@@ -134,6 +147,7 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
if (isSorting) {
this.#propertySorter.setModel(this._propertyStructure);
} else {
// TODO: Make a more proper way to disable sorting:
this.#propertySorter.setModel([]);
}
}

View File

@@ -28,6 +28,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement {
config: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
...SORTER_CONFIG,
// TODO: Missing handlers to work properly: performItemMove and performItemRemove
performItemInsert: async (args) => {
if (!this._groups) return false;
const oldIndex = this._groups.findIndex((group) => group.id! === args.item.id);

View File

@@ -38,6 +38,7 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
config: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
...SORTER_CONFIG,
// TODO: Missing handlers to work properly: performItemMove and performItemRemove
performItemInsert: async (args) => {
if (!this._tabs) return false;
const oldIndex = this._tabs.findIndex((tab) => tab.id! === args.item.id);

View File

@@ -60,6 +60,18 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
this.#pickerContext.setSelection(ids);
}
@property({ type: String })
startNodeId?: string;
@property({ type: String })
filter?: string;
@property({ type: Boolean })
showOpenButton?: boolean;
@property({ type: Boolean })
ignoreUserStartNodes?: boolean;
@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.
@@ -73,6 +85,10 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
constructor() {
super();
}
connectedCallback() {
super.connectedCallback();
this.addValidator(
'rangeUnderflow',
@@ -90,23 +106,37 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected _openPicker() {
// TODO: Configure the content picker, with `startNodeId`, `filter` and `ignoreUserStartNodes` [LK]
console.log("_openPicker", [this.startNodeId, this.filter, this.ignoreUserStartNodes]);
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
protected _openItem(item: DocumentItemResponseModel) {
// TODO: Implement the Content editing infinity editor. [LK]
console.log('TODO: _openItem', item);
}
protected getFormElement() {
return undefined;
}
render() {
return html`
${this._items
? html` <uui-ref-list
return html` ${this.#renderItems()} ${this.#renderAddButton()} `;
}
#renderItems() {
if (!this._items) return;
// TODO: Add sorting. [LK]
return html`<uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this._renderItem(item),
)}
</uui-ref-list>`
: ''}
${this.#renderAddButton()}
`;
</uui-ref-list>`;
}
#renderAddButton() {
@@ -114,7 +144,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
return html`<uui-button
id="add-button"
look="placeholder"
@click=${() => this.#pickerContext.openPicker()}
@click=${this._openPicker}
label=${this.localize.term('general_choose')}></uui-button>`;
}
@@ -122,8 +152,9 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
if (!item.id) return;
return html`
<uui-ref-node name=${ifDefined(item.name)} detail=${ifDefined(item.id)}>
<!-- TODO: implement is trashed <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> -->
${this._renderIsTrashed(item)}
<uui-action-bar slot="actions">
${this._renderOpenButton(item)}
<uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
label="Remove document ${item.name}"
@@ -134,6 +165,18 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
`;
}
private _renderIsTrashed(item: DocumentItemResponseModel) {
if (!item.isTrashed) return;
return html`<uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag>`;
}
private _renderOpenButton(item: DocumentItemResponseModel) {
if (!this.showOpenButton) return;
return html`<uui-button @click=${() => this._openItem(item)} label="Open document ${item.name}"
>${this.localize.term('general_open')}</uui-button
>`;
}
static styles = [
css`
#add-button {

View File

@@ -1,5 +1,5 @@
import { UmbMediaTypePickerContext } from './input-media-type.context.js';
import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
@@ -73,6 +73,10 @@ export class UmbInputMediaTypeElement extends FormControlMixin(UmbLitElement) {
constructor() {
super();
}
connectedCallback() {
super.connectedCallback();
this.addValidator(
'rangeUnderflow',
@@ -94,33 +98,63 @@ export class UmbInputMediaTypeElement extends FormControlMixin(UmbLitElement) {
return undefined;
}
#openPicker() {
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
render() {
console.log('ITEMS', this._items);
return html` ${this.#renderItems()} ${this.#renderAddButton()} `;
}
#renderItems() {
if (!this._items) return;
return html`
<uui-ref-list>${this._items?.map((item) => this._renderItem(item))}</uui-ref-list>
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label="open"
>Add</uui-button
<uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this.#renderItem(item),
)}</uui-ref-list
>
`;
}
private _renderItem(item: MediaTypeItemResponseModel) {
if (!item.id) return;
//TODO: Using uui-ref-node as we don't have a uui-ref-media-type yet.
#renderAddButton() {
if (this.max > 0 && this.selectedIds.length >= this.max) return;
return html`
<uui-ref-node name=${ifDefined(item.name)}>
<uui-button
id="add-button"
look="placeholder"
@click=${this.#openPicker}
label="${this.localize.term('general_choose')}"
>${this.localize.term('general_choose')}</uui-button
>
`;
}
#renderItem(item: MediaTypeItemResponseModel) {
if (!item.id) return;
return html`
<uui-ref-node-document-type name=${ifDefined(item.name)}>
${this.#renderIcon(item)}
<uui-action-bar slot="actions">
<uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
label="Remove Media Type ${item.name}"
>Remove</uui-button
>${this.localize.term('general_remove')}</uui-button
>
</uui-action-bar>
</uui-ref-node>
</uui-ref-node-document-type>
`;
}
#renderIcon(item: MediaTypeItemResponseModel) {
if (!item.icon) return;
return html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`;
}
static styles = [
css`
#add-button {

View File

@@ -18,7 +18,7 @@ const SORTER_CONFIG: UmbSorterConfig<MediaTypePropertyTypeResponseModel> = {
return element.getAttribute('data-umb-property-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: MediaTypePropertyTypeResponseModel) => {
return container.querySelector('data-umb-property-id=[' + modelEntry.id + ']');
return container.querySelector('[data-umb-property-id=' + modelEntry.id + ']');
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',
@@ -45,6 +45,19 @@ export class UmbMediaTypeWorkspaceViewEditPropertiesElement extends UmbLitElemen
performItemRemove: (args) => {
return this._propertyStructureHelper.removeProperty(args.item.id!);
},
performItemMove: (args) => {
this._propertyStructureHelper.removeProperty(args.item.id!);
let sortOrder = 0;
if (this._propertyStructure.length > 0) {
if (args.newIndex === 0) {
sortOrder = (this._propertyStructure[0].sortOrder ?? 0) - 1;
} else {
sortOrder =
(this._propertyStructure[Math.min(args.newIndex, this._propertyStructure.length - 1)].sortOrder ?? 0) + 1;
}
}
return this._propertyStructureHelper.insertProperty(args.item, sortOrder);
},
});
private _containerId: string | undefined;

View File

@@ -28,6 +28,7 @@ export class UmbMediaTypeWorkspaceViewEditTabElement extends UmbLitElement {
config: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
...SORTER_CONFIG,
// TODO: Missing handlers to work properly: performItemMove and performItemRemove
performItemInsert: async (args) => {
if (!this._groups) return false;
const oldIndex = this._groups.findIndex((group) => group.id! === args.item.id);

View File

@@ -38,6 +38,7 @@ export class UmbMediaTypeWorkspaceViewEditElement extends UmbLitElement implemen
config: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
...SORTER_CONFIG,
// TODO: Missing handlers for these: performItemRemove, performItemMove
performItemInsert: async (args) => {
if (!this._tabs) return false;
const oldIndex = this._tabs.findIndex((tab) => tab.id! === args.item.id);

View File

@@ -60,6 +60,15 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
this.#pickerContext.setSelection(ids);
}
@property({ type: String })
filter?: string;
@property({ type: Boolean })
showOpenButton?: boolean;
@property({ type: Boolean })
ignoreUserStartNodes?: boolean;
@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.
@@ -73,6 +82,10 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
constructor() {
super();
}
connectedCallback() {
super.connectedCallback();
this.addValidator(
'rangeUnderflow',
@@ -90,18 +103,41 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected _openPicker() {
// TODO: Configure the media picker, with `filter` and `ignoreUserStartNodes` [LK]
console.log('_openPicker', [this.filter, this.ignoreUserStartNodes]);
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
protected _openItem(item: MediaItemResponseModel) {
// TODO: Implement the Content editing infinity editor. [LK]
console.log('TODO: _openItem', item);
}
protected getFormElement() {
return undefined;
}
render() {
return html` ${this._items?.map((item) => this.#renderItem(item))} ${this.#renderButton()} `;
return html` ${this.#renderItems()} ${this.#renderButton()} `;
}
#renderItems() {
if (!this._items) return;
// TODO: Add sorting. [LK]
return html` ${this._items?.map((item) => this.#renderItem(item))} `;
}
#renderButton() {
if (this._items && this.max && this._items.length >= this.max) return;
return html`
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label=${this.localize.term('general_choose')}>
<uui-button
id="add-button"
look="placeholder"
@click=${this._openPicker}
label=${this.localize.term('general_choose')}>
<uui-icon name="icon-add"></uui-icon>
${this.localize.term('general_choose')}
</uui-button>
@@ -109,12 +145,14 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
}
#renderItem(item: MediaItemResponseModel) {
// TODO: `file-ext` value has been hardcoded here. Find out if API model has value for it. [LK]
// TODO: How to handle the `showOpenButton` option? [LK]
return html`
<uui-card-media
name=${ifDefined(item.name === null ? undefined : item.name)}
detail=${ifDefined(item.id)}
file-ext="jpg">
<!-- <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> -->
${this._renderIsTrashed(item)}
<uui-action-bar slot="actions">
<uui-button label="Copy media">
<uui-icon name="icon-documents"></uui-icon>
@@ -127,6 +165,11 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
`;
}
private _renderIsTrashed(item: MediaItemResponseModel) {
if (!item.isTrashed) return;
return html`<uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag>`;
}
static styles = [
css`
:host {

View File

@@ -6,7 +6,7 @@ import type { MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/backen
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
@customElement('umb-input-member-type')
export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) {
export class UmbInputMemberTypeElement extends FormControlMixin(UmbLitElement) {
/**
* This is a minimum amount of selected items in this input.
* @type {number}
@@ -94,32 +94,28 @@ export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) {
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected _openPicker() {
protected getFormElement() {
return undefined;
}
#openPicker() {
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
protected getFormElement() {
return undefined;
}
render() {
return html`
${this.#renderItems()}
${this.#renderAddButton()}
`;
return html` ${this.#renderItems()} ${this.#renderAddButton()} `;
}
#renderItems() {
if (!this._items) return;
// TODO: Add sorting. [LK]
return html`
<uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this._renderItem(item),
(item) => this.#renderItem(item),
)}</uui-ref-list
>
`;
@@ -131,17 +127,18 @@ export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) {
<uui-button
id="add-button"
look="placeholder"
@click=${this._openPicker}
@click=${this.#openPicker}
label="${this.localize.term('general_choose')}"
>${this.localize.term('general_choose')}</uui-button
>
`;
}
private _renderItem(item: MemberTypeItemResponseModel) {
#renderItem(item: MemberTypeItemResponseModel) {
if (!item.id) return;
return html`
<uui-ref-node-document-type name=${ifDefined(item.name)}>
${this.#renderIcon(item)}
<uui-action-bar slot="actions">
<uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
@@ -153,6 +150,11 @@ export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) {
`;
}
#renderIcon(item: MemberTypeItemResponseModel) {
if (!item.icon) return;
return html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`;
}
static styles = [
css`
#add-button {
@@ -162,10 +164,10 @@ export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) {
];
}
export default UmbMemberTypeInputElement;
export default UmbInputMemberTypeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-member-type': UmbMemberTypeInputElement;
'umb-input-member-type': UmbInputMemberTypeElement;
}
}

View File

@@ -0,0 +1,158 @@
import { RichTextRuleModelSortable, UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
import { UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR } from '../../manifests.js';
import { StylesheetRichTextEditorStyleModalValue } from './stylesheet-workspace-view-rich-text-editor-style-sidebar.element.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext, UmbModalToken } from '@umbraco-cms/backoffice/modal';
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { css, html, customElement, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import './stylesheet-workspace-view-rich-text-editor-rule.element.js';
export const UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL = new UmbModalToken<
never,
StylesheetRichTextEditorStyleModalValue
>(UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR, {
modal: {
type: 'sidebar',
size: 'medium',
},
value: { rule: null },
});
const SORTER_CONFIG: UmbSorterConfig<RichTextRuleModel> = {
compareElementToModel: (element: HTMLElement, model: RichTextRuleModel) => {
return element.getAttribute('data-umb-rule-name') === model.name;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: RichTextRuleModel) => {
return container.querySelector('[data-umb-rule-name' + modelEntry.name + ']');
},
identifier: 'stylesheet-rules-sorter',
itemSelector: 'umb-stylesheet-rich-text-editor-rule',
containerSelector: '#rules-container',
};
@customElement('umb-stylesheet-workspace-view-rich-text-editor')
export class UmbStylesheetWorkspaceViewRichTextEditorElement extends UmbLitElement {
@state()
_rules: RichTextRuleModelSortable[] = [];
#context?: UmbStylesheetWorkspaceContext;
private _modalContext?: UmbModalManagerContext;
#sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
// TODO: Implement correctly, this code below was not correct:
/*
performItemInsert: ({ item, newIndex }) => {
return this.#context?.findNewSortOrder(item, newIndex) ?? false;
},
performItemRemove: () => {
//defined so the default does not run
return true;
},
*/
// End of todo comment.
onChange: ({ model }) => {
const oldValue = this._rules;
this._rules = model;
this.requestUpdate('_rules', oldValue);
},
});
constructor() {
super();
this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#context = workspaceContext as UmbStylesheetWorkspaceContext;
this.observe(this.#context.rules, (rules) => {
this._rules = rules;
this.#sorter.setModel(this._rules);
});
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
openModal = (rule: RichTextRuleModelSortable | null = null) => {
if (!this._modalContext) throw new Error('Modal context not found');
const modal = this._modalContext.open(UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL, {
value: {
rule,
},
});
modal?.onSubmit().then((result) => {
if (result.rule) {
this.#context?.setRules([...this._rules, { ...result.rule, sortOrder: this._rules.length }]);
}
});
};
removeRule = (rule: RichTextRuleModelSortable) => {
const rules = this._rules?.filter((r) => r.name !== rule.name);
this.#context?.setRules(rules);
};
render() {
return html` <uui-box headline="Rich text editor styles">
<div id="box-row">
<p id="description">Define the styles that should be available in the rich text editor for this stylesheet.</p>
<div id="rules">
<div id="rules-container">
${repeat(
this._rules,
(rule) => rule?.name ?? '' + rule?.sortOrder ?? '',
(rule) =>
html`<umb-stylesheet-rich-text-editor-rule
.rule=${rule}
data-umb-rule-name="${ifDefined(rule?.name)}"></umb-stylesheet-rich-text-editor-rule>`,
)}
</div>
<uui-button label="Add rule" look="primary" @click=${() => this.openModal(null)}>Add</uui-button>
</div>
</div>
</uui-box>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
width: 100%;
}
#box-row {
display: flex;
gap: var(--uui-size-layout-1);
}
#description {
margin-top: 0;
flex: 0 0 250px;
}
#rules {
flex: 1 1 auto;
max-width: 600px;
}
uui-box {
margin: var(--uui-size-layout-1);
}
`,
];
}
export default UmbStylesheetWorkspaceViewRichTextEditorElement;
declare global {
interface HTMLElementTagNameMap {
'umb-stylesheet-workspace-view-rich-text-editor': UmbStylesheetWorkspaceViewRichTextEditorElement;
}
}

View File

@@ -74,7 +74,6 @@
"@umbraco-cms/backoffice/localization": ["src/packages/core/localization"],
"@umbraco-cms/backoffice/macro": ["src/packages/core/macro"],
"@umbraco-cms/backoffice/menu": ["src/packages/core/menu"],
"@umbraco-cms/backoffice/meta": ["src/packages/core/meta"],
"@umbraco-cms/backoffice/modal": ["src/packages/core/modal"],
"@umbraco-cms/backoffice/notification": ["src/packages/core/notification"],
"@umbraco-cms/backoffice/picker-input": ["src/packages/core/picker-input"],

View File

@@ -74,7 +74,6 @@ export default {
'@umbraco-cms/backoffice/localization': './src/packages/core/localization/index.ts',
'@umbraco-cms/backoffice/macro': './src/packages/core/macro/index.ts',
'@umbraco-cms/backoffice/menu': './src/packages/core/menu/index.ts',
'@umbraco-cms/backoffice/meta': './src/packages/core/meta/index.ts',
'@umbraco-cms/backoffice/modal': './src/packages/core/modal/index.ts',
'@umbraco-cms/backoffice/notification': './src/packages/core/notification/index.ts',
'@umbraco-cms/backoffice/picker-input': './src/packages/core/picker-input/index.ts',