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);
+ }
+ }
+ }
}
}