diff --git a/libs/dotcms-webcomponents/src/components.d.ts b/libs/dotcms-webcomponents/src/components.d.ts index 33662527239b..30bdea29f791 100644 --- a/libs/dotcms-webcomponents/src/components.d.ts +++ b/libs/dotcms-webcomponents/src/components.d.ts @@ -624,10 +624,22 @@ export namespace Components { * Reset properties of the field, clear value and emit events. */ "reset": () => Promise; + /** + * (optional) Allows unique keys only + */ + "uniqueKeys": boolean; /** * Value of the field */ "value": string; + /** + * (optional) The string containing the value to be parsed for whitelist key/value + */ + "whiteList": string; + /** + * (optional) The string to use in the empty option of whitelist dropdown key/value item + */ + "whiteListEmptyOptionLabel": string; } interface DotLabel { /** @@ -1024,6 +1036,10 @@ export namespace Components { * (optional) Disables all form interaction */ "disabled": boolean; + /** + * (optional) Label for the empty option in white-list select + */ + "emptyDropdownOptionLabel": string; /** * (optional) The string to use in the key input label */ @@ -1040,6 +1056,10 @@ export namespace Components { * (optional) Placeholder for the value input text */ "valuePlaceholder": string; + /** + * (optional) The string to use for white-list key/values + */ + "whiteList": string; } interface KeyValueTable { /** @@ -1961,10 +1981,22 @@ declare namespace LocalJSX { * (optional) Text that will be shown when required is set and condition is not met */ "requiredMessage"?: string; + /** + * (optional) Allows unique keys only + */ + "uniqueKeys"?: boolean; /** * Value of the field */ "value"?: string; + /** + * (optional) The string containing the value to be parsed for whitelist key/value + */ + "whiteList"?: string; + /** + * (optional) The string to use in the empty option of whitelist dropdown key/value item + */ + "whiteListEmptyOptionLabel"?: string; } interface DotLabel { /** @@ -2346,6 +2378,10 @@ declare namespace LocalJSX { * (optional) Disables all form interaction */ "disabled"?: boolean; + /** + * (optional) Label for the empty option in white-list select + */ + "emptyDropdownOptionLabel"?: string; /** * (optional) The string to use in the key input label */ @@ -2370,6 +2406,10 @@ declare namespace LocalJSX { * (optional) Placeholder for the value input text */ "valuePlaceholder"?: string; + /** + * (optional) The string to use for white-list key/values + */ + "whiteList"?: string; } interface KeyValueTable { /** @@ -2392,6 +2432,10 @@ declare namespace LocalJSX { * Emit the index of the item deleted from the list */ "onDelete"?: (event: CustomEvent) => void; + /** + * Emit the notification of list reordered + */ + "onReorder"?: (event: CustomEvent) => void; } interface IntrinsicElements { "dot-asset-drop-zone": DotAssetDropZone; diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.scss b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.scss index 714e91fcedba..5156c892caed 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.scss +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.scss @@ -1,22 +1,28 @@ key-value-form form { - display: flex; - align-items: center; - - button { - margin: 0; - } - - input { - margin: 0 1rem 0 0.5rem; - } - label { align-items: center; display: flex; flex-grow: 1; + } + + table { + width: 100%; input { - flex-grow: 1; + width: 100%; } } + + .key-value-table-form__key { + width: 45%; + } + + .key-value-table-form__value { + width: 45%; + } + + .key-value-table-form__action { + text-align: right; + width: 10%; + } } diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.tsx b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.tsx index cb4572c88f1a..c4fcbab04eb0 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.tsx +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/key-value-form.tsx @@ -1,4 +1,4 @@ -import { Component, Prop, State, Element, Event, EventEmitter, h } from '@stencil/core'; +import { Component, Prop, State, Element, Event, EventEmitter, h, Watch } from '@stencil/core'; import { DotKeyValueField } from '../../../../../models'; const DEFAULT_VALUE = { key: '', value: '' }; @@ -45,6 +45,18 @@ export class DotKeyValueComponent { }) valueLabel = 'Value'; + /** (optional) Label for the empty option in white-list select */ + @Prop({ + reflect: true + }) + emptyDropdownOptionLabel = 'Pick an option'; + + /** (optional) The string to use for white-list key/values */ + @Prop({ + reflect: true + }) + whiteList = ''; + /** Emit the added value, key/value pair */ @Event() add: EventEmitter; @@ -56,12 +68,48 @@ export class DotKeyValueComponent { @State() inputs: DotKeyValueField = { ...DEFAULT_VALUE }; + @State() + selectedWhiteListKey = ''; + + @Watch('selectedWhiteListKey') + selectedWhiteListKeyWatch(): void { + /* */ + } + + private whiteListArray = {}; + + componentWillLoad(): void { + this.whiteListArray = this.whiteList.length ? JSON.parse(this.whiteList) : ''; + } + render() { const buttonDisabled = this.isButtonDisabled(); return (
-
- - - + + + + + ); } + private getWhiteListForm(buttonDisabled: boolean): JSX.Element { + return ( + + {this.getWhiteListKeysDropdown()} + + {this.selectedWhiteListKey ? this.getWhiteListValueControl() : null} + + + + + + ); + } + + private getWhiteListValueControl(): boolean { + return this.whiteListArray[this.selectedWhiteListKey].length ? ( + this.getWhiteListValuesDropdown() + ) : ( + this.lostFocus.emit(e)} + onInput={(event: Event) => this.setValue(event)} + placeholder={this.valuePlaceholder} + type="text" + value={this.inputs.value} + /> + ); + } + + private getWhiteListKeysDropdown(): JSX.Element { + return ( + + ); + } + + private getWhiteListValuesDropdown(): JSX.Element { + return ( + + ); + } + + private changeWhiteListKey(event: Event): void { + event.stopImmediatePropagation(); + this.clearForm(); + const target = event.target as HTMLInputElement; + this.selectedWhiteListKey = target.value; + this.setValue(event); + } + + private changeWhiteListValue(event: Event): void { + event.stopImmediatePropagation(); + this.setValue(event); + } + private isButtonDisabled(): boolean { return !this.isFormValid() || this.disabled || null; } @@ -129,7 +257,7 @@ export class DotKeyValueComponent { } private focusKeyInputField(): void { - const input: HTMLInputElement = this.el.querySelector('input[name="key"]'); + const input: HTMLInputElement = this.el.querySelector('[name="key"]'); input.focus(); } } diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/readme.md b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/readme.md index 6764ebbf3add..819d54f97663 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/readme.md +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-form/readme.md @@ -7,14 +7,16 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------ | ------------------- | ----------------------------------------------------- | --------- | --------- | -| `addButtonLabel` | `add-button-label` | (optional) Label for the add item button | `string` | `'Add'` | -| `disabled` | `disabled` | (optional) Disables all form interaction | `boolean` | `false` | -| `keyLabel` | `key-label` | (optional) The string to use in the key input label | `string` | `'Key'` | -| `keyPlaceholder` | `key-placeholder` | (optional) Placeholder for the key input text | `string` | `''` | -| `valueLabel` | `value-label` | (optional) The string to use in the value input label | `string` | `'Value'` | -| `valuePlaceholder` | `value-placeholder` | (optional) Placeholder for the value input text | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| -------------------------- | ----------------------------- | ---------------------------------------------------------- | --------- | ------------------ | +| `addButtonLabel` | `add-button-label` | (optional) Label for the add item button | `string` | `'Add'` | +| `disabled` | `disabled` | (optional) Disables all form interaction | `boolean` | `false` | +| `emptyDropdownOptionLabel` | `empty-dropdown-option-label` | (optional) Label for the empty option in white-list select | `string` | `'Pick an option'` | +| `keyLabel` | `key-label` | (optional) The string to use in the key input label | `string` | `'Key'` | +| `keyPlaceholder` | `key-placeholder` | (optional) Placeholder for the key input text | `string` | `''` | +| `valueLabel` | `value-label` | (optional) The string to use in the value input label | `string` | `'Value'` | +| `valuePlaceholder` | `value-placeholder` | (optional) Placeholder for the value input text | `string` | `''` | +| `whiteList` | `white-list` | (optional) The string to use for white-list key/values | `string` | `''` | ## Events diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.scss b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.scss index 16dfcdeb3c03..01e24687db8f 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.scss +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.scss @@ -1,16 +1,13 @@ key-value-table { - table { width: 100%; } .key-value-table-wc__key { - padding-left: 5%; width: 45%; } .key-value-table-wc__value { - padding-left: 9%; width: 45%; } @@ -18,4 +15,8 @@ key-value-table { text-align: right; width: 10%; } + + .key-value-table-wc__placeholder-transit { + border-bottom: 1px solid; + } } diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.tsx b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.tsx index bcd9ff55d5cd..115197a9542e 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.tsx +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/key-value-table.tsx @@ -30,6 +30,12 @@ export class KeyValueTableComponent { @Event() delete: EventEmitter; + /** Emit the notification of list reordered */ + @Event() + reorder: EventEmitter; + + dragSrcEl = null; + render() { return ( @@ -38,6 +44,126 @@ export class KeyValueTableComponent { ); } + componentDidLoad() { + this.bindDraggableEvents(); + } + + componentDidUpdate() { + this.bindDraggableEvents(); + } + + // D&D - BEGIN + + private bindDraggableEvents() { + const rows = document.querySelectorAll('key-value-table tr'); + rows.forEach((row) => { + row.setAttribute('draggable', 'true'); + + row.removeEventListener('dragstart', this.handleDragStart.bind(this), false); + row.removeEventListener('dragenter', this.handleDragEnter, false); + row.removeEventListener('dragover', this.handleDragOver.bind(this), false); + row.removeEventListener('dragleave', this.handleDragLeave, false); + row.removeEventListener('drop', this.handleDrop.bind(this), false); + row.removeEventListener('dragend', this.handleDragEnd.bind(this), false); + + row.addEventListener('dragstart', this.handleDragStart.bind(this), false); + row.addEventListener('dragenter', this.handleDragEnter, false); + row.addEventListener('dragover', this.handleDragOver.bind(this), false); + row.addEventListener('dragleave', this.handleDragLeave, false); + row.addEventListener('drop', this.handleDrop.bind(this), false); + row.addEventListener('dragend', this.handleDragEnd.bind(this), false); + }); + } + + private removeElementById(elemId) { + document.getElementById(elemId).remove(); + } + + private isPlaceholderInDOM() { + return !!document.getElementById('dotKeyValuePlaceholder'); + } + + private isCursorOnUpperSide(cursor, { top, bottom }) { + return cursor.y - top < (bottom - top) / 2; + } + + private setPlaceholder() { + const placeholder = document.createElement('tr'); + placeholder.id = 'dotKeyValuePlaceholder'; + placeholder.classList.add('key-value-table-wc__placeholder-transit'); + return placeholder; + } + + private insertBeforeElement(newElem, element) { + element.parentNode.insertBefore(newElem, element); + } + + private insertAfterElement(newElem, element) { + element.parentNode.insertBefore(newElem, element.nextSibling); + } + + private handleDragStart(e) { + this.dragSrcEl = e.target; + } + + private handleDragOver(e) { + if (e.preventDefault) { + e.preventDefault(); + } + if (this.dragSrcEl != e.target) { + const contentlet = e.target.closest('tr'); + const contentletPlaceholder = this.setPlaceholder(); + if (this.isPlaceholderInDOM()) { + this.removeElementById('dotKeyValuePlaceholder'); + } + + if (this.isCursorOnUpperSide(e, contentlet.getBoundingClientRect())) { + this.insertBeforeElement(contentletPlaceholder, contentlet); + } else { + this.insertAfterElement(contentletPlaceholder, contentlet); + } + } + return false; + } + + private handleDragEnter(e) { + e.target.classList.add('over'); + } + + private handleDragLeave(e) { + e.target.classList.remove('over'); + } + + private handleDrop(e) { + if (e.stopPropagation) { + e.stopPropagation(); // stops the browser from redirecting. + } + if (this.dragSrcEl != e.target) { + document + .getElementById('dotKeyValuePlaceholder') + .insertAdjacentElement('afterend', this.dragSrcEl); + } + + return false; + } + + private handleDragEnd() { + const rows = document.querySelectorAll('key-value-table tr'); + rows.forEach(function (row) { + row.classList.remove('over'); + }); + + try { + this.removeElementById('dotKeyValuePlaceholder'); + } catch (e) { + /**/ + } + + this.reorder.emit(); + } + + // D&D - END + private onDelete(index: number): void { this.delete.emit(index); } diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/readme.md b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/readme.md index 667aa7ceb080..5fc03176829c 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/readme.md +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/components/key-value-table/readme.md @@ -17,9 +17,10 @@ ## Events -| Event | Description | Type | -| -------- | ------------------------------------------------ | --------------------- | -| `delete` | Emit the index of the item deleted from the list | `CustomEvent` | +| Event | Description | Type | +| --------- | ------------------------------------------------ | --------------------- | +| `delete` | Emit the index of the item deleted from the list | `CustomEvent` | +| `reorder` | Emit the notification of list reordered | `CustomEvent` | ## Dependencies diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx index 9b7cc93596a1..ded86572d923 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx @@ -85,6 +85,12 @@ export class DotKeyValueComponent { }) disabled = false; + /** (optional) Allows unique keys only */ + @Prop({ + reflect: true + }) + uniqueKeys = false; + /** (optional) Placeholder for the key input text in the key-value-form */ @Prop({ reflect: true @@ -121,6 +127,18 @@ export class DotKeyValueComponent { }) listDeleteLabel: string; + /** (optional) The string to use in the empty option of whitelist dropdown key/value item */ + @Prop({ + reflect: true + }) + whiteListEmptyOptionLabel: string; + + /** (optional) The string containing the value to be parsed for whitelist key/value */ + @Prop({ + reflect: true + }) + whiteList: string; + @State() status: DotFieldStatus; @State() @@ -159,11 +177,42 @@ export class DotKeyValueComponent { this.emitChanges(); } + @Listen('reorder') + reorderItemsHandler(event: CustomEvent) { + event.stopImmediatePropagation(); + + // Hack to clean the items in DOM without showing "No values" label + this.items = [{ key: ' ', value: '' }]; + + const keys = document.querySelectorAll('.key-value-table-wc__key'); + const values = document.querySelectorAll('.key-value-table-wc__value'); + let keyValueRawData = ''; + + for (let i = 0, total = keys.length; i < total; i++) { + keyValueRawData += `${keys[i].innerHTML}|${values[i].innerHTML},`; + } + + // Timeout to let the DOM get cleaned and then repopulate with list of keyValues + setTimeout(() => { + this.items = [ + ...getDotOptionsFromFieldValue( + keyValueRawData.substring(0, keyValueRawData.length - 1) + ).map(mapToKeyValue) + ]; + this.refreshStatus(); + this.emitChanges(); + }, 100); + } + @Listen('add') addItemHandler({ detail }: CustomEvent): void { - this.items = [...this.items, detail]; - this.refreshStatus(); - this.emitChanges(); + const itemExists = this.items.some((item: DotKeyValueField) => item.key === detail.key); + + if ((this.uniqueKeys && !itemExists) || !this.uniqueKeys) { + this.items = [...this.items, detail]; + this.refreshStatus(); + this.emitChanges(); + } } componentWillLoad(): void { @@ -188,10 +237,12 @@ export class DotKeyValueComponent { onLostFocus={this.blurHandler.bind(this)} add-button-label={this.formAddButtonLabel} disabled={this.isDisabled()} + empty-dropdown-option-label={this.whiteListEmptyOptionLabel} key-label={this.formKeyLabel} key-placeholder={this.formKeyPlaceholder} value-label={this.formValueLabel} value-placeholder={this.formValuePlaceholder} + white-list={this.whiteList} /> { diff --git a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/readme.md b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/readme.md index 0f5c66cd7626..55c1434d5cdc 100644 --- a/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/readme.md +++ b/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/readme.md @@ -7,21 +7,24 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ---------------------- | ------------------------ | -------------------------------------------------------------------------------- | --------- | -------------------------- | -| `disabled` | `disabled` | (optional) Disables field's interaction | `boolean` | `false` | -| `formAddButtonLabel` | `form-add-button-label` | (optional) Label for the add button in the key-value-form | `string` | `undefined` | -| `formKeyLabel` | `form-key-label` | (optional) The string to use in the key label in the key-value-form | `string` | `undefined` | -| `formKeyPlaceholder` | `form-key-placeholder` | (optional) Placeholder for the key input text in the key-value-form | `string` | `undefined` | -| `formValueLabel` | `form-value-label` | (optional) The string to use in the value label in the key-value-form | `string` | `undefined` | -| `formValuePlaceholder` | `form-value-placeholder` | (optional) Placeholder for the value input text in the key-value-form | `string` | `undefined` | -| `hint` | `hint` | (optional) Hint text that suggest a clue of the field | `string` | `''` | -| `label` | `label` | (optional) Text to be rendered next to input field | `string` | `''` | -| `listDeleteLabel` | `list-delete-label` | (optional) The string to use in the delete button of a key/value item | `string` | `undefined` | -| `name` | `name` | Name that will be used as ID | `string` | `''` | -| `required` | `required` | (optional) Determine if it is mandatory | `boolean` | `false` | -| `requiredMessage` | `required-message` | (optional) Text that will be shown when required is set and condition is not met | `string` | `'This field is required'` | -| `value` | `value` | Value of the field | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| --------------------------- | ------------------------------- | ------------------------------------------------------------------------------------- | --------- | -------------------------- | +| `disabled` | `disabled` | (optional) Disables field's interaction | `boolean` | `false` | +| `formAddButtonLabel` | `form-add-button-label` | (optional) Label for the add button in the key-value-form | `string` | `undefined` | +| `formKeyLabel` | `form-key-label` | (optional) The string to use in the key label in the key-value-form | `string` | `undefined` | +| `formKeyPlaceholder` | `form-key-placeholder` | (optional) Placeholder for the key input text in the key-value-form | `string` | `undefined` | +| `formValueLabel` | `form-value-label` | (optional) The string to use in the value label in the key-value-form | `string` | `undefined` | +| `formValuePlaceholder` | `form-value-placeholder` | (optional) Placeholder for the value input text in the key-value-form | `string` | `undefined` | +| `hint` | `hint` | (optional) Hint text that suggest a clue of the field | `string` | `''` | +| `label` | `label` | (optional) Text to be rendered next to input field | `string` | `''` | +| `listDeleteLabel` | `list-delete-label` | (optional) The string to use in the delete button of a key/value item | `string` | `undefined` | +| `name` | `name` | Name that will be used as ID | `string` | `''` | +| `required` | `required` | (optional) Determine if it is mandatory | `boolean` | `false` | +| `requiredMessage` | `required-message` | (optional) Text that will be shown when required is set and condition is not met | `string` | `'This field is required'` | +| `uniqueKeys` | `unique-keys` | (optional) Allows unique keys only | `boolean` | `false` | +| `value` | `value` | Value of the field | `string` | `''` | +| `whiteList` | `white-list` | (optional) The string containing the value to be parsed for whitelist key/value | `string` | `undefined` | +| `whiteListEmptyOptionLabel` | `white-list-empty-option-label` | (optional) The string to use in the empty option of whitelist dropdown key/value item | `string` | `undefined` | ## Events