diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index 0022d3cdda..97c39f77f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -99,6 +99,7 @@ export class UmbPickerInputContext< content: 'Are you sure you want to remove this item', confirmLabel: 'Remove', }); + this.#removeItem(unique); } 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 cddf17f5e4..df5e20479a 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 @@ -2,15 +2,17 @@ import { UmbDocumentPickerContext } from './input-document.context.js'; import { classMap, css, customElement, html, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; -@customElement('umb-input-document') +const elementName = 'umb-input-document'; + +@customElement(elementName) export class UmbInputDocumentElement extends UmbFormControlMixin( UmbLitElement, ) { @@ -73,7 +75,7 @@ export class UmbInputDocumentElement extends UmbFormControlMixin) { @@ -124,28 +126,24 @@ export class UmbInputDocumentElement extends UmbFormControlMixin this.minMessage, - () => !!this.min && this.#pickerContext.getSelection().length < this.min, + () => !!this.min && this.selection.length < this.min, ); this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this.#pickerContext.getSelection().length > this.max, + () => !!this.max && this.selection.length > this.max, ); this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); } - protected override getFormElement() { - return undefined; - } - #isDraft(item: UmbDocumentItemModel) { return item.variants[0]?.state === 'Draft'; } - #pickableFilter: (item: UmbDocumentItemModel) => boolean = (item) => { + #pickableFilter = (item: UmbDocumentItemModel): boolean => { if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { return this.allowedContentTypeIds.includes(item.documentType.unique); } @@ -160,7 +158,7 @@ export class UmbInputDocumentElement extends UmbFormControlMixin= this.max) return; + if (this.selection.length >= this.max) return; return html` ${this.#renderOpenButton(item)} - this.#removeItem(item)} label=${this.localize.term('general_remove')}> + this.#onRemove(item)} label=${this.localize.term('general_remove')}> `; @@ -229,7 +227,7 @@ export class UmbInputDocumentElement extends UmbFormControlMixin) { @@ -164,20 +164,16 @@ export class UmbInputMediaElement extends UmbFormControlMixin this.minMessage, - () => !!this.min && this.#pickerContext.getSelection().length < this.min, + () => !!this.min && this.selection.length < this.min, ); this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this.#pickerContext.getSelection().length > this.max, + () => !!this.max && this.selection.length > this.max, ); } - protected override getFormElement() { - return undefined; - } - - #pickableFilter: (item: UmbMediaItemModel) => boolean = (item) => { + #pickableFilter = (item: UmbMediaItemModel): boolean => { if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { return this.allowedContentTypeIds.includes(item.mediaType.unique); } @@ -192,8 +188,9 @@ export class UmbInputMediaElement extends UmbFormControlMixin x.unique !== item.unique); } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index fdb67e7a79..20f5188702 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -9,7 +9,9 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/rou import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; -@customElement('umb-input-member') +const elementName = 'umb-input-member'; + +@customElement(elementName) export class UmbInputMemberElement extends UmbFormControlMixin( UmbLitElement, ) { @@ -72,7 +74,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin) { @@ -123,24 +125,20 @@ export class UmbInputMemberElement extends UmbFormControlMixin this.minMessage, - () => !!this.min && this.#pickerContext.getSelection().length < this.min, + () => !!this.min && this.selection.length < this.min, ); this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this.#pickerContext.getSelection().length > this.max, + () => !!this.max && this.selection.length > this.max, ); this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems'); } - protected override getFormElement() { - return undefined; - } - - #pickableFilter: (item: UmbMemberItemModel) => boolean = (item) => { + #pickableFilter = (item: UmbMemberItemModel): boolean => { if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { return this.allowedContentTypeIds.includes(item.memberType.unique); } @@ -154,7 +152,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin= this.max) return nothing; - return html``; + if (this.selection.length >= this.max) return nothing; + return html` + + `; } #renderItem(item: UmbMemberItemModel) { @@ -190,7 +190,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin ${this.#renderOpenButton(item)} - this.#removeItem(item)} label=${this.localize.term('general_remove')}> + this.#onRemove(item)} label=${this.localize.term('general_remove')}> `; @@ -210,7 +210,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin `; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts index 5396d89c1d..b404d9a51e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts @@ -2,66 +2,73 @@ import type { UmbContentPickerSource } from '../../types.js'; import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; -import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; -import type { UmbInputMemberElement } from '@umbraco-cms/backoffice/member'; import type { UmbReferenceByUniqueAndType } from '@umbraco-cms/backoffice/models'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; const elementName = 'umb-input-content'; + @customElement(elementName) export class UmbInputContentElement extends UmbFormControlMixin( UmbLitElement, ) { - protected override getFormElement() { - return undefined; - } - - private _type: UmbContentPickerSource['type'] = 'content'; - @property({ type: Object, attribute: false }) + @property() public set type(newType: UmbContentPickerSource['type']) { - const oldType = this._type; - if (newType?.toLowerCase() !== this._type) { - this._type = newType?.toLowerCase() as UmbContentPickerSource['type']; + const oldType = this.#type; + if (newType?.toLowerCase() !== this.#type) { + this.#type = newType?.toLowerCase() as UmbContentPickerSource['type']; this.requestUpdate('type', oldType); } } public get type(): UmbContentPickerSource['type'] { - return this._type; + return this.#type; } + #type: UmbContentPickerSource['type'] = 'content'; @property({ type: Number }) min = 0; + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + @property({ type: Number }) max = 0; + @property({ type: String, attribute: 'max-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + @property({ type: Object, attribute: false }) startNode?: UmbTreeStartNode; - private _allowedContentTypeIds: Array = []; @property() public set allowedContentTypeIds(value: string) { - this._allowedContentTypeIds = value ? value.split(',') : []; + this.#allowedContentTypeIds = value ? value.split(',') : []; } public get allowedContentTypeIds(): string { - return this._allowedContentTypeIds.join(','); + return this.#allowedContentTypeIds.join(','); } + #allowedContentTypeIds: Array = []; @property({ type: Boolean }) showOpenButton?: boolean; - #entityTypeLookup = { content: 'document', media: 'media', member: 'member' }; + @property({ type: Array }) + public set selection(values: Array) { + this.#selection = values?.map((item) => item.unique) ?? []; + } + public get selection(): Array { + return this.#selection.map((id) => ({ type: this.#entityTypeLookup[this.#type], unique: id })); + } - // TODO: to be consistent with other pickers, this should be named `selection` [NL] + /** @deprecated Please use `selection` instead. This property will be removed in Umbraco 15. */ @property({ type: Array }) public set items(items: Array) { - this.#selection = items?.map((item) => item.unique) ?? []; + this.selection = items; } + /** @deprecated Please use `selection` instead. This property will be removed in Umbraco 15. */ public get items(): Array { - return this.#selection.map((id) => ({ type: this.#entityTypeLookup[this._type], unique: id })); + return this.selection; } @property({ type: String }) @@ -72,38 +79,22 @@ export class UmbInputContentElement extends UmbFormControlMixin 0 ? this.#selection.join(',') : undefined; } + #entityTypeLookup = { content: 'document', media: 'media', member: 'member' }; + #selection: Array = []; - #onChange(event: CustomEvent) { - switch (this._type) { - case 'content': - { - const input = event.target as UmbInputDocumentElement; - this.#selection = input.selection; - this.value = input.selection.join(','); - } - break; - case 'media': { - const input = event.target as UmbInputMediaElement; - this.#selection = input.selection; - this.value = input.selection.join(','); - break; - } - case 'member': { - const input = event.target as UmbInputMemberElement; - this.#selection = input.selection; - this.value = input.selection.join(','); - break; - } - default: - break; - } + override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector(`umb-input-${this.#entityTypeLookup[this.#type]}`)!); + } + #onChange(event: CustomEvent & { target: { selection: string[] | undefined } }) { + this.#selection = event.target.selection ?? []; + this.value = this.#selection.join(','); this.dispatchEvent(new UmbChangeEvent()); } override render() { - switch (this._type) { + switch (this.#type) { case 'content': return this.#renderDocumentPicker(); case 'media': @@ -116,34 +107,46 @@ export class UmbInputContentElement extends UmbFormControlMixin`; + return html` + + `; } #renderMediaPicker() { - return html``; + return html` + + `; } #renderMemberPicker() { - return html``; + return html` + + `; } static override styles = [ @@ -156,7 +159,7 @@ export class UmbInputContentElement extends UmbFormControlMixin { let element: UmbInputContentElement; beforeEach(async () => { - element = await fixture(html` `); + element = await fixture(html``); }); it('is defined with its own instance', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index 6c250bb1fa..c40a756769 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -3,25 +3,39 @@ import type { UmbInputContentElement } from './components/input-content/index.js import type { UmbContentPickerSource, UmbContentPickerSourceType } from './types.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_DOCUMENT_ENTITY_TYPE } from '@umbraco-cms/backoffice/document'; +import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UMB_MEDIA_ENTITY_TYPE } from '@umbraco-cms/backoffice/media'; import { UMB_MEMBER_ENTITY_TYPE } from '@umbraco-cms/backoffice/member'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; // import of local component import './components/input-content/index.js'; +type UmbContentPickerValueType = UmbInputContentElement['selection']; + +const elementName = 'umb-property-editor-ui-content-picker'; + /** * @element umb-property-editor-ui-content-picker */ -@customElement('umb-property-editor-ui-content-picker') -export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { +@customElement(elementName) +export class UmbPropertyEditorUIContentPickerElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ @property({ type: Array }) - value: UmbInputContentElement['items'] = []; + public override set value(value: UmbContentPickerValueType | undefined) { + this.#value = value; + } + public override get value(): UmbContentPickerValueType | undefined { + return this.#value; + } + #value?: UmbContentPickerValueType = []; @state() _type: UmbContentPickerSource['type'] = 'content'; @@ -29,9 +43,15 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple @state() _min = 0; + @state() + _minMessage = ''; + @state() _max = Infinity; + @state() + _maxMessage = ''; + @state() _allowedContentTypeUniques?: string | null; @@ -64,15 +84,28 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple this.#dynamicRoot = startNode.dynamicRoot; } - this._min = Number(config.getValueByAlias('minNumber')) || 0; - this._max = Number(config.getValueByAlias('maxNumber')) || Infinity; + this._min = this.#parseInt(config.getValueByAlias('minNumber'), 0); + this._max = this.#parseInt(config.getValueByAlias('maxNumber'), Infinity); this._allowedContentTypeUniques = config.getValueByAlias('filter'); this._showOpenButton = config.getValueByAlias('showOpenButton'); + + this._minMessage = `${this.localize.term('validation_minCount')} ${this._min} ${this.localize.term('validation_items')}`; + this._maxMessage = `${this.localize.term('validation_maxCount')} ${this._max} ${this.localize.term('validation_itemsSelected')}`; + + // NOTE: Run validation immediately, to notify if the value is outside of min/max range. [LK] + if (this._min > 0 || this._max < Infinity) { + this.checkValidity(); + } } - override connectedCallback() { - super.connectedCallback(); + #parseInt(value: unknown, fallback: number): number { + const num = Number(value); + return !isNaN(num) && num > 0 ? num : fallback; + } + + override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-content')!); this.#setPickerRootUnique(); } @@ -96,7 +129,7 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple } #onChange(event: CustomEvent & { target: UmbInputContentElement }) { - this.value = event.target.items; + this.value = event.target.selection; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } @@ -106,22 +139,26 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple ? { unique: this._rootUnique, entityType: this._rootEntityType } : undefined; - return html``; + return html` + + `; } } -export default UmbPropertyEditorUIContentPickerElement; +export { UmbPropertyEditorUIContentPickerElement as element }; declare global { interface HTMLElementTagNameMap { - 'umb-property-editor-ui-content-picker': UmbPropertyEditorUIContentPickerElement; + [elementName]: UmbPropertyEditorUIContentPickerElement; } }