diff --git a/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/index.ts b/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/index.ts index bf689650a7..4c50c3a641 100644 --- a/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/index.ts +++ b/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/index.ts @@ -3,7 +3,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'dashboard', - name: 'Example Dataset Workspace View', + name: 'Example Dataset Dashboard', alias: 'example.dashboard.dataset', element: () => import('./dataset-dashboard.js'), weight: 900, diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/README.md b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/README.md new file mode 100644 index 0000000000..3dc57f3788 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/README.md @@ -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. diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/index.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/index.ts new file mode 100644 index 0000000000..0771859fe9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/index.ts @@ -0,0 +1,15 @@ +import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'dashboard', + name: 'Example Sorter Dashboard', + alias: 'example.dashboard.dataset', + element: () => import('./sorter-dashboard.js'), + weight: 900, + meta: { + label: 'Sorter example', + pathname: 'sorter-example', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts new file mode 100644 index 0000000000..0e88318ab3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts @@ -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` + +
+
Notice this example still only support single group of Sorter.
+ + +
+
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts new file mode 100644 index 0000000000..4ce3125463 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts @@ -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 = { + 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(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` +
+ ${repeat( + this.items, + (item) => item.name, + (item) => + html` + + `, + )} +
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-item.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-item.ts new file mode 100644 index 0000000000..495370bb00 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-item.ts @@ -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} + + + `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 8ea91660fd..66f4d5dc44 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -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", diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts index 0e4883c957..473e4d8498 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts @@ -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 })); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts index 9015e3a44a..ca4efd70b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tree-picker-source/input-tree-picker-source.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tree-picker-source/input-tree-picker-source.element.ts index 90f59172a5..4cb56e5dec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tree-picker-source/input-tree-picker-source.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tree-picker-source/input-tree-picker-source.element.ts @@ -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 = (event.target).nodeId; break; case 'media': - this.nodeId = (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}>`; } - #renderTypeMedia() { - const nodeId = this.nodeId ? [this.nodeId] : []; - //TODO => MediaTypes - return html``; - } - - #renderTypeMember() { - const nodeId = this.nodeId ? [this.nodeId] : []; - //TODO => Members - return html``; - } - static styles = [ css` :host { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-input.element.ts index 044004b46e..36ab21138a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-input.element.ts @@ -21,7 +21,7 @@ const SORTER_CONFIG: UmbSorterConfig = { 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 = { 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()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string.element.ts index 88a9c656f2..3697da0646 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string.element.ts @@ -16,7 +16,7 @@ const SORTER_CONFIG: UmbSorterConfig = { 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 = { 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); }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts index 7d8a2e2b3d..8260f8f636 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/data-type-flow-input/data-type-flow-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/data-type-flow-input/data-type-flow-input.element.ts index 181c11f8de..e3429f218f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/data-type-flow-input/data-type-flow-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/data-type-flow-input/data-type-flow-input.element.ts @@ -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(() => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/ref-data-type/ref-data-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/ref-data-type/ref-data-type.element.ts index e54290fea0..4af4380ec5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/ref-data-type/ref-data-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/components/ref-data-type/ref-data-type.element.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts index d20f934b85..6c30d005bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts @@ -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 { [key: string]: Array; @@ -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 } } }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/repository/detail/data-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/repository/detail/data-type-detail.server.data-source.ts index 7e84a6b2a3..d6cdf52c56 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/repository/detail/data-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/repository/detail/data-type-detail.server.data-source.ts @@ -40,8 +40,8 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource, }; @@ -86,15 +86,15 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource - items.filter((item) => item.propertyEditorUiAlias === propertyEditorUiAlias), - ); + return this._data.asObservablePart((items) => items.filter((item) => item.editorUiAlias === propertyEditorUiAlias)); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/types.ts index e0655e512a..b783f836d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/types.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts index 95068965ce..71498bfea6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts @@ -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([], (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(propertyAlias: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.modal-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.modal-token.ts new file mode 100644 index 0000000000..e4af72f9ec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.modal-token.ts @@ -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, + 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, 'entityType'>, UmbWorkspaceValue>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/index.ts index edea71c7a5..cd8d2359be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/index.ts @@ -1 +1,2 @@ export * from './data-type-workspace.context-token.js'; +export * from './data-type-workspace.modal-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/views/info/workspace-view-data-type-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/views/info/workspace-view-data-type-info.element.ts index e315847fa5..d7c14bc646 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/views/info/workspace-view-data-type-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/views/info/workspace-view-data-type-info.element.ts @@ -41,11 +41,11 @@ export class UmbWorkspaceViewDataTypeInfoElement extends UmbLitElement implement
${this._dataType?.unique}
-
${this._dataType?.propertyEditorAlias}
+
${this._dataType?.editorAlias}
-
${this._dataType?.propertyEditorUiAlias}
+
${this._dataType?.editorUiAlias}
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/index.ts index 5bcdd57eaf..a5e6406910 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/meta/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/meta/index.ts deleted file mode 100644 index 90b526bc17..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/meta/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import packageJson from '../../../../package.json'; - -export const umbMeta = { - name: 'Bellissima', - clientName: packageJson.name, - clientVersion: packageJson.version, -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/workspace-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/workspace-modal.token.ts index 3f64bfaa1f..93b568b232 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/workspace-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/workspace-modal.token.ts @@ -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 { entityType: string; - preset: Partial; + preset: Partial; } // TODO: It would be good with a WorkspaceValueBaseType, to avoid the hardcoded type for unique here: diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-picker/property-editor-ui-tree-picker-source-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-picker/property-editor-ui-tree-picker-source-picker.element.ts index 67ddcc214b..8f7bb83ce7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-picker/property-editor-ui-tree-picker-source-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-picker/property-editor-ui-tree-picker-source-picker.element.ts @@ -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() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-type-picker/property-editor-ui-tree-picker-source-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-type-picker/property-editor-ui-tree-picker-source-type-picker.element.ts index 16b11ffa40..af31d985d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-type-picker/property-editor-ui-tree-picker-source-type-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/config/source-type-picker/property-editor-ui-tree-picker-source-type-picker.element.ts @@ -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 = (event.target).selectedIds; + this.#setValue((event.target).selectedIds); break; case 'media': - this.value = (event.target).selectedIds; + this.#setValue((event.target).selectedIds); break; case 'member': - this.value = (event.target).selectedIds; + this.#setValue((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`

No source type found

`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts index a505a8dd77..43c5f393ee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts @@ -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() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts index fa1b7f6662..8c4923b9f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts @@ -61,27 +61,23 @@ function destroyPreventEvent(element: Element) { element.removeAttribute('draggable'); } -type INTERNAL_UmbSorterConfig = { - compareElementToModel: (el: HTMLElement, modelEntry: T) => boolean; - querySelectModelToElement: (container: HTMLElement, modelEntry: T) => HTMLElement | null; +type INTERNAL_UmbSorterConfig = { + 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; - toController: UmbSorterController; - }) => void; + onStart?: (argument: { item: T; element: ElementType }) => void; + onChange?: (argument: { item: T; model: Array }) => 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 = { 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; performItemInsert?: (argument: { item: T; newIndex: number }) => Promise | boolean; performItemRemove?: (argument: { item: T }) => Promise | boolean; }; // External type with some properties optional, as they have defaults: -export type UmbSorterConfig = Omit< - INTERNAL_UmbSorterConfig, - 'placeholderClass' | 'ignorerSelector' | 'containerSelector' +export type UmbSorterConfig = Omit< + INTERNAL_UmbSorterConfig, + 'ignorerSelector' | 'containerSelector' > & - Partial, 'placeholderClass' | 'ignorerSelector' | 'containerSelector'>>; + Partial, 'ignorerSelector' | 'containerSelector'>>; /** * @export @@ -114,55 +111,58 @@ export type UmbSorterConfig = Omit< * @implements {UmbControllerInterface} * @description This controller can make user able to sort items. */ -export class UmbSorterController implements UmbController { +export class UmbSorterController implements UmbController { #host; - #config: INTERNAL_UmbSorterConfig; + #config: INTERNAL_UmbSorterConfig; #observer; #model: Array = []; #rqaId?: number; #containerElement!: HTMLElement; - #currentContainerVM = this; + + #currentContainerCtrl: UmbSorterController = 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 | null = null; + #lastIndicationContainerCtrl: UmbSorterController | null = null; public get controllerAlias() { return this.#config.identifier; } - constructor(host: UmbControllerHostElement, config: UmbSorterConfig) { + constructor(host: UmbControllerHostElement, config: UmbSorterConfig) { 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; + this.#config = config as INTERNAL_UmbSorterConfig; 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 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 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 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 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); } - - 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.#config.placeholderAttr) { + this.#currentElement?.setAttribute(this.#config.placeholderAttr, ''); } - - if (!this.#scrollElement) { - this.#scrollElement = getParentScrollElement(this.#containerElement, true); + } + #removePlaceholderStyle() { + if (this.#config.placeholderClass) { + this.#currentElement?.classList.remove(this.#config.placeholderClass); } + if (this.#config.placeholderAttr) { + this.#currentElement?.removeAttribute(this.#config.placeholderAttr); + } + } - 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 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 implements UmbController { this.#dragY = 0; }; - handleDragMove = (event: DragEvent) => { + #handleDragMove = (event: DragEvent) => { if (!this.#currentElement) { return; } @@ -386,13 +398,13 @@ export class UmbSorterController 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 implements UmbController { return; } - // If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement. + let toBeCurrentContainerCtrl: UmbSorterController | 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 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 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 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 implements UmbController { //ownerVM: this.#currentContainerVM.ownerVM, }); } - this.moveCurrentElement(); + this.#updateDragMove(); return; } } @@ -534,7 +549,9 @@ export class UmbSorterController 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 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, 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 | 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; + + // 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) { - const movingItem = fromVm.getItemOfElement(element); - if (!movingItem) { + public async moveItemInModel(newIndex: number, element: ElementType, fromCtrl: UmbSorterController) { + 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 (result === false) { - return false; + if (this.#config.performItemMove) { + const result = await this.#config.performItemMove({ item, newIndex, oldIndex }); + if (result === false) { + return false; + } + } else { + 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 { - this.#model.splice(newIndex, 0, movingItem); - } + // Not a local move: - const eventData = { item: movingItem, fromController: fromVm, toController: this }; - if (fromVm !== this) { - fromVm.notifySync(eventData); + if ((await fromCtrl.removeItem(item)) !== true) { + console.error('Sync could not remove item'); + return false; + } + + 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, item: T) { + updateAllowIndication(controller: UmbSorterController, 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 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 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(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx index fdf53c52be..496f9061ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx @@ -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 = [...] +type MySortEntryType = {...}; +const awesomeModel: Array = [...]; const MY_SORTER_CONFIG: UmbSorterConfig = { 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 = {...} 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, }); } ``` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts index 968dec1b9b..4c2864dd6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts @@ -12,7 +12,7 @@ const SORTER_CONFIG: UmbSorterConfig = { 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 = [...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
    - ${model.map( + ${this._items.map( (entry) => html`
  • ${entry.value} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/components/input-tree/input-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/components/input-tree/input-tree.element.ts index 93c78f79d6..234a127b12 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/components/input-tree/input-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/components/input-tree/input-tree.element.ts @@ -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 = []; #onChange(event: CustomEvent) { - this.value = (event.target as UmbInputDocumentElement).selectedIds.join(','); + 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,40 +90,50 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) { render() { switch (this._type) { case 'content': - return html``; + return this.#renderContentPicker(); case 'media': - return html``; + return this.#renderMediaPicker(); case 'member': - return html` - `; + return this.#renderMemberPicker(); default: - return html`Type could not be found`; + return html`

    Type could not be found.

    `; } } + #renderContentPicker() { + return html``; + } + + #renderMediaPicker() { + return html``; + } + + #renderMemberPicker() { + return html``; + } + static styles = [ css` p { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts index ad9de9b02a..d9f31b2ebe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts @@ -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` ${this._items?.map((item) => this._renderItem(item))} - ${this.#renderAddButton()}`; + return html` ${this.#renderItems()} ${this.#renderAddButton()} `; + } + + #renderItems() { + if (!this._items) return nothing; + return html` + ${repeat( + this._items, + (item) => item.id, + (item) => this.#renderItem(item), + )} + `; } #renderAddButton() { - if (this.max === 1 && this.selectedIds.length === 1) return nothing; - return html` - Add - `; + if (this.max > 0 && this.selectedIds.length >= this.max) return nothing; + return html` + ${this.localize.term('general_choose')} + `; } - private _renderItem(item: DocumentTypeItemResponseModel) { + #renderItem(item: DocumentTypeItemResponseModel) { if (!item.id) return; return html` + ${this.#renderIcon(item)} `; + } + static styles = [ css` #add-button { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts index c5f2fd9dcd..fbfee1c86f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts @@ -18,7 +18,7 @@ const SORTER_CONFIG: UmbSorterConfig = { 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([]); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts index 99c81ac4b4..5ab781f63d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts @@ -28,6 +28,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement { config: UmbSorterConfig = { ...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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts index 0b5156443f..c6a496ad70 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts @@ -38,6 +38,7 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple config: UmbSorterConfig = { ...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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 504d487406..186f6cd874 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -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` ${repeat( - this._items, - (item) => item.id, - (item) => this._renderItem(item), - )} - ` - : ''} - ${this.#renderAddButton()} - `; + return html` ${this.#renderItems()} ${this.#renderAddButton()} `; + } + + #renderItems() { + if (!this._items) return; + // TODO: Add sorting. [LK] + return html`${repeat( + this._items, + (item) => item.id, + (item) => this._renderItem(item), + )} + `; } #renderAddButton() { @@ -114,7 +144,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { return html` this.#pickerContext.openPicker()} + @click=${this._openPicker} label=${this.localize.term('general_choose')}>`; } @@ -122,8 +152,9 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { if (!item.id) return; return html` - + ${this._renderIsTrashed(item)} + ${this._renderOpenButton(item)} 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`Trashed`; + } + + private _renderOpenButton(item: DocumentItemResponseModel) { + if (!this.showOpenButton) return; + return html` this._openItem(item)} label="Open document ${item.name}" + >${this.localize.term('general_open')}`; + } + static styles = [ css` #add-button { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts index 787e5be130..4b977a296d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts @@ -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` - ${this._items?.map((item) => this._renderItem(item))} - this.#pickerContext.openPicker()} label="open" - >Add${repeat( + this._items, + (item) => item.id, + (item) => this.#renderItem(item), + )} `; } - 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` - + ${this.localize.term('general_choose')} + `; + } + + #renderItem(item: MediaTypeItemResponseModel) { + if (!item.id) return; + return html` + + ${this.#renderIcon(item)} this.#pickerContext.requestRemoveItem(item.id!)} label="Remove Media Type ${item.name}" - >Remove${this.localize.term('general_remove')} - + `; } + #renderIcon(item: MediaTypeItemResponseModel) { + if (!item.icon) return; + return html``; + } + static styles = [ css` #add-button { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-properties.element.ts index 1e4c45984b..84dd842b41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-properties.element.ts @@ -18,7 +18,7 @@ const SORTER_CONFIG: UmbSorterConfig = { 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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-tab.element.ts index fd56eccc7e..8fd560c1fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit-tab.element.ts @@ -28,6 +28,7 @@ export class UmbMediaTypeWorkspaceViewEditTabElement extends UmbLitElement { config: UmbSorterConfig = { ...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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts index 13534b91be..86b41a2c33 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts @@ -38,6 +38,7 @@ export class UmbMediaTypeWorkspaceViewEditElement extends UmbLitElement implemen config: UmbSorterConfig = { ...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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index 2b20086885..94530be00a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -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` - this.#pickerContext.openPicker()} label=${this.localize.term('general_choose')}> + ${this.localize.term('general_choose')} @@ -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` - + ${this._renderIsTrashed(item)} @@ -127,6 +165,11 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { `; } + private _renderIsTrashed(item: MediaItemResponseModel) { + if (!item.isTrashed) return; + return html`Trashed`; + } + static styles = [ css` :host { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-types/components/input-member-type/input-member-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-types/components/input-member-type/input-member-type.element.ts index 35cb32f71a..6ac9e16ad7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-types/components/input-member-type/input-member-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-types/components/input-member-type/input-member-type.element.ts @@ -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` ${repeat( this._items, (item) => item.id, - (item) => this._renderItem(item), + (item) => this.#renderItem(item), )} `; @@ -131,17 +127,18 @@ export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) { ${this.localize.term('general_choose')} `; } - private _renderItem(item: MemberTypeItemResponseModel) { + #renderItem(item: MemberTypeItemResponseModel) { if (!item.id) return; return html` + ${this.#renderIcon(item)} this.#pickerContext.requestRemoveItem(item.id!)} @@ -153,6 +150,11 @@ export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) { `; } + #renderIcon(item: MemberTypeItemResponseModel) { + if (!item.icon) return; + return html``; + } + 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; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.ts new file mode 100644 index 0000000000..64908209ae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.ts @@ -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 = { + 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` +
    +

    Define the styles that should be available in the rich text editor for this stylesheet.

    +
    +
    + ${repeat( + this._rules, + (rule) => rule?.name ?? '' + rule?.sortOrder ?? '', + (rule) => + html``, + )} +
    + this.openModal(null)}>Add +
    +
    +
    `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 9da73f3f4d..de24c48990 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -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"], diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index ddd950e659..9cf753f9a6 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -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',