diff --git a/client/res/templates/fields/enum/detail.tpl b/client/res/templates/fields/enum/detail.tpl index e42147465b..4540b437c4 100644 --- a/client/res/templates/fields/enum/detail.tpl +++ b/client/res/templates/fields/enum/detail.tpl @@ -1,11 +1,19 @@ {{#if isNotEmpty}} -{{#if style}} -{{/if}}{{valueTranslated}}{{#if style}}{{/if}} + {{~#if hasColor~}} +   + {{~/if~}} + {{~#if style~}} + + {{~/if~}} + {{valueTranslated}} + {{~#if style~}}{{~/if~}} {{else}} -{{#if valueIsSet}} -{{translate 'None'}} -{{else}} - -{{/if}} -{{/if}} + {{~#if valueIsSet~}} + {{translate 'None'}} + {{~else~}} + + {{~/if~}} +{{/if~}} diff --git a/client/res/templates/fields/enum/list.tpl b/client/res/templates/fields/enum/list.tpl index bec29f0c65..ee3e997ca5 100644 --- a/client/res/templates/fields/enum/list.tpl +++ b/client/res/templates/fields/enum/list.tpl @@ -1,7 +1,16 @@ -{{#if isNotEmpty}} -{{#if style}} -{{/if}}{{valueTranslated}}{{#if style}}{{/if}} -{{/if}} +{{#if isNotEmpty~}} + {{~#if hasColor~}} +   + {{~/if~}} + {{~#if style~}} + + {{~/if~}} + {{valueTranslated}} + {{~#if style}}{{~/if~}} +{{~/if~}} diff --git a/client/src/ui/select.js b/client/src/ui/select.js index 8bca5814a2..bff450cb95 100644 --- a/client/src/ui/select.js +++ b/client/src/ui/select.js @@ -42,6 +42,13 @@ import Selectize from 'lib!selectize'; * @property {'value'|'text'|'$order'|'$score'} [sortBy='$order'] Item sorting. * @property {'asc'|'desc'} [sortDirection='asc'] Sort direction. * @property {function()} [onFocus] On-focus callback. + * @property { + * function({value: string}): { + * text?: string, + * style?: 'default'|'danger'|'success'|'warning'|'info'|null, + * color?: string|null, + * } + * } [itemHandler] Handles an item to override the label or add a style. As of v9.4.0. */ /** @@ -107,6 +114,44 @@ const Select = { $relativeParent = $modalBody; } + /** + * + * @param {{value: string, text: string}} item + * @return {{ + * className: string, + * text: string, + * prefixElement?: HTMLElement, + * }} + */ + const prepareItem = item => { + let className = itemClasses[item.value] ?? ''; + let text = item.text; + let prefixElement; + + if (options.itemHandler) { + const result = options.itemHandler({value: item.value}); + + if (result.style) { + className = 'text-' + result.style; + } + + if (result.text != null) { + text = result.text; + } + + if (result.color !== undefined) { + prefixElement = document.createElement('span'); + prefixElement.className = 'color-icon fas fa-square text-soft'; + + if (result.color) { + prefixElement.style.color = result.color; + } + } + } + + return {className, text, prefixElement}; + }; + // noinspection JSUnusedGlobalSymbols const selectizeOptions = { sortField: [{field: options.sortBy, direction: options.sortDirection}], @@ -121,25 +166,42 @@ const Select = { $relativeParent: $relativeParent, render: { item: function (data) { - return $('
') - .addClass('item') - .addClass(itemClasses[data.value] || '') - .text(data.text) - .get(0).outerHTML; - }, - option: function (data) { - const $div = $('
') - .addClass('option') - .addClass(data.value === '' ? 'selectize-dropdown-emptyoptionlabel' : '') - .addClass(itemClasses[data.value] || '') - .val(data.value) - .text(data.text); + const item = prepareItem(data); - if (data.text === '') { - $div.html(' '); + const div = document.createElement('div'); + div.className = item.className; + div.classList.add('item'); + div.textContent = item.text; + + if (item.prefixElement && data.text !== '') { + div.prepend(item.prefixElement); } - return $div.get(0).outerHTML; + return div.outerHTML; + }, + option: function (data) { + const item = prepareItem(data); + + const div = document.createElement('div'); + div.className = item.className; + div.classList.add('option'); + + div.textContent = item.text; + div.setAttribute('data-value', data.value); + + if (data.value === '') { + div.classList.add('selectize-dropdown-emptyoptionlabel') + } + + if (data.text === '') { + div.innerHTML = ' '; + } + + if (item.prefixElement && data.text !== '') { + div.prepend(item.prefixElement); + } + + return div.outerHTML; }, }, onDelete: function (values) { diff --git a/client/src/views/fields/enum.js b/client/src/views/fields/enum.js index a44098249c..811cab4b06 100644 --- a/client/src/views/fields/enum.js +++ b/client/src/views/fields/enum.js @@ -46,6 +46,8 @@ class EnumFieldView extends BaseFieldView { * module:views/fields/base~params & * Object. * } [params] Parameters. + * @property {module:views/fields/enumeration~optionItemHandler} [optionItemHandler] + * Handles an option item to override the label or add a style. As of v9.4.0. */ /** @@ -63,6 +65,16 @@ class EnumFieldView extends BaseFieldView { * @property {Object.} [translatedOptions] Option translations. */ + /** + * @typedef { + * function({value: string}): { + * text?: string, + * style?: 'default'|'danger'|'success'|'warning'|'info'|null, + * color?: string|null, + * } + * } module:views/fields/enumeration~optionItemHandler + */ + /** * @param { * module:views/fields/enumeration~options & @@ -103,6 +115,12 @@ class EnumFieldView extends BaseFieldView { */ nativeSelect = false; + /** + * @protected + * @type {module:views/fields/enumeration~optionItemHandler|null} + */ + optionItemHandler = null + // noinspection JSCheckFunctionSignatures /** @inheritDoc */ data() { @@ -159,6 +177,15 @@ class EnumFieldView extends BaseFieldView { data.nativeSelect = this.nativeSelect; } + if (this.isReadMode() && this.optionItemHandler && this.model.attributes[this.name]) { + const item = this.optionItemHandler({value: this.model.attributes[this.name]}); + + if (item.color != null) { + data.color = item.color; + data.hasColor = true; + } + } + // noinspection JSValidateTypes return data; } @@ -172,6 +199,10 @@ class EnumFieldView extends BaseFieldView { } } + if (this.options.optionItemHandler) { + this.optionItemHandler = this.options.optionItemHandler; + } + this.styleMap = this.params.style || this.model.getFieldParam(this.name, 'style') || {}; let optionsPath = this.params.optionsPath; @@ -435,6 +466,7 @@ class EnumFieldView extends BaseFieldView { if ((this.isEditMode() || this.isSearchMode()) && !this.nativeSelect) { Select.init(this.$element, { matchAnyWord: true, + itemHandler: this.optionItemHandler, }); } } diff --git a/frontend/less/espo/misc/selectize/custom.less b/frontend/less/espo/misc/selectize/custom.less index 271f6010a3..8d0c3759fa 100644 --- a/frontend/less/espo/misc/selectize/custom.less +++ b/frontend/less/espo/misc/selectize/custom.less @@ -63,6 +63,10 @@ vertical-align: top; text-overflow: ellipsis; white-space: nowrap; + + .color-icon { + padding-right: var(--5px); + } } cursor: default; @@ -71,6 +75,14 @@ .selectize-dropdown { z-index: 1031; + + .selectize-dropdown-content { + .option { + .color-icon { + padding-right: var(--5px); + } + } + } } }