')
+ .addClass('item')
+ .addClass(itemClasses[data.value] || '')
+ .text(data.text)
+ .get(0).outerHTML;
+ },
+ option: function (data) {
+ let $div = $('
')
+ .addClass('option')
+ .addClass(data.value === '' ? 'selectize-dropdown-emptyoptionlabel' : '')
+ .addClass(itemClasses[data.value] || '')
+ .val(data.value)
+ .text(data.text);
+
+ if (data.text === '') {
+ $div.html(' ');
+ }
+
+ return $div.get(0).outerHTML;
+ },
+ },
+ onDelete: function (values) {
+ while (values.length) {
+ this.removeItem(values.pop(), true);
+ }
+
+ this.showInput();
+ this.positionDropdown();
+ this.refreshOptions(true);
+ },
+ };
+
+ if (!options.matchAnyWord) {
+ /** @this Selectize */
+ selectizeOptions.score = function (search) {
+ let score = this.getScoreFunction(search);
+
+ search = search.toLowerCase();
+
+ return function (item) {
+ if (item.text.toLowerCase().indexOf(search) === 0) {
+ return score(item);
+ }
+
+ return 0;
+ };
+ };
+ }
+
+ $el.selectize(selectizeOptions);
+ },
+
+ /**
+ * Focus.
+ *
+ * @param {JQuery} $el An element.
+ * @param {{noTrigger?: boolean}} [options] Options.
+ */
+ focus: function ($el, options) {
+ options = options || {};
+
+ let selectize = $el.get(0).selectize;
+
+ if (options.noTrigger) {
+ selectize.focusNoTrigger = true;
+ }
+
+ selectize.focus();
+
+ setTimeout(() => selectize.focusNoTrigger = false, 100);
+ },
+
+ /**
+ * Set options.
+ *
+ * @param {JQuery} $el An element.
+ * @param {{value: string, label: string}[]} options Options.
+ */
+ setOptions: function ($el, options) {
+ let selectize = $el.get(0).selectize;
+
+ selectize.clearOptions(true);
+ selectize.load(callback => {
+ callback(
+ options.map(item => {
+ return {
+ value: item.value,
+ text: item.label,
+ };
+ })
+ );
+ });
+ },
+
+ /**
+ * Set value.
+ *
+ * @param {JQuery} $el An element.
+ * @param {string} value A value.
+ */
+ setValue: function ($el, value) {
+ let selectize = $el.get(0).selectize;
+
+ selectize.setValue(value, true);
+ },
+
+ /**
+ * @private
+ * @param {module:ui/select~Options} options
+ * @return {module:ui/select~Options}
+ */
+ applyDefaultOptions: function (options) {
+ options = Espo.Utils.clone(options);
+
+ let defaults = {
+ selectOnTab: false,
+ matchAnyWord: false,
+ };
+
+ for (let key in defaults) {
+ if (key in options) {
+ continue;
+ }
+
+ options[key] = defaults[key];
+ }
+
+ return options;
+ },
+
+ /**
+ * @private
+ */
+ loadEspoSelectPlugin: function () {
+ if ('espo_select' in Selectize.plugins) {
+ return;
+ }
+
+ const IS_MAC = /Mac/.test(navigator.userAgent);
+ const KEY_BACKSPACE = 8;
+
+ Selectize.define('espo_select', function () {
+ let self = this;
+
+ this.refreshOptions = (function () {
+ let original = self.refreshOptions;
+
+ return function () {
+ if (self.focusNoTrigger) {
+ original.apply(this, [false]);
+ return;
+ }
+
+ original.apply(this, arguments);
+ };
+ })();
+
+ this.onOptionSelect = (function () {
+ let original = self.onOptionSelect;
+
+ return function (e) {
+ original.apply(this, arguments);
+
+ self.selectedValue = $(e.currentTarget).attr('data-value');
+ };
+ })();
+
+ this.open = (function() {
+ let original = self.open;
+
+ return function () {
+ let toProcess = !(self.isLocked || self.isOpen);
+
+ original.apply(this, arguments);
+
+ if (!toProcess) {
+ return;
+ }
+
+ let $selected = self.$dropdown.find('.selected');
+
+ if (!$selected.length) {
+ return;
+ }
+
+ self.$dropdown
+ .find('.selectize-dropdown-content')
+ .scrollTop($selected.get(0).offsetTop);
+ };
+ })();
+
+ this.onMouseDown = (function() {
+ let original = self.onMouseDown;
+
+ return function (e) {
+ if (self.isOpen) {
+ self.closedByMouseDown = true;
+ }
+
+ return original.apply(this, arguments);
+ };
+ })();
+
+ this.onFocus = (function() {
+ let original = self.onFocus;
+
+ return function (e) {
+ self.selectedValue = self.getValue();
+
+ if (self.closedByMouseDown) {
+ self.closedByMouseDown = false;
+
+ return;
+ }
+
+ self.closedByMouseDown = false;
+
+ return original.apply(this, arguments);
+ };
+ })();
+
+ this.revertValue = function () {
+ if (this.selectedValue !== null) {
+ this.setValue(this.selectedValue, true);
+ }
+
+ this.selectedValue = null;
+ };
+
+ this.onBlur = (function() {
+ let original = self.onBlur;
+
+ return function () {
+ self.revertValue();
+
+ self.$control_input.css({width: '4px'});
+
+ return original.apply(this, arguments);
+ };
+ })();
+
+ this.onKeyDown = (function() {
+ let original = self.onKeyDown;
+
+ return function (e) {
+ if (e.code === 'Enter' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
+ return;
+ }
+
+ if (e.code === 'Escape') {
+ if (self.isOpen || !self.isInputHidden) {
+ e.stopPropagation();
+ }
+
+ if (self.isOpen) {
+ self.close();
+ }
+
+ if (!self.isInputHidden) {
+ self.hideInput();
+ }
+
+ self.addItem(this.selectedValue, true);
+ }
+
+ if (self.isFull() || self.isInputHidden) {
+ if (
+ e.key.length === 1 &&
+ (
+ e.key.match(/[a-z]/i) ||
+ e.key.match(/[0-9]/)
+ )
+ ) {
+ let keyCode = e.keyCode;
+ e.keyCode = KEY_BACKSPACE;
+
+ self.deleteSelection(e);
+
+ //self.clear();
+
+ e.keyCode = keyCode;
+ }
+ }
+
+ return original.apply(this, arguments);
+ };
+ })();
+ });
+ },
+ };
+
+ return Select;
+});
diff --git a/client/src/views/fields/base.js b/client/src/views/fields/base.js
index 536a9e4346..5e33743478 100644
--- a/client/src/views/fields/base.js
+++ b/client/src/views/fields/base.js
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
-define('views/fields/base', ['view'], function (Dep) {
+define('views/fields/base', ['view', 'ui/select'], function (Dep, /** module:ui/select*/Select) {
/**
* A base field view. Can be in different modes. Each mode uses a separate template.
@@ -780,6 +780,14 @@ define('views/fields/base', ['view'], function (Dep) {
if (this.hasRequiredMarker()) {
this.hideRequiredSign();
}
+
+ if (this.isSearchMode()) {
+ let $searchType = this.$el.find('select.search-type');
+
+ if ($searchType.length) {
+ Select.init($searchType, {matchAnyWord: true});
+ }
+ }
});
if ((this.isDetailMode() || this.isEditMode()) && this.tooltip) {
diff --git a/client/src/views/fields/currency.js b/client/src/views/fields/currency.js
index 3b89d0af3f..853a736acd 100644
--- a/client/src/views/fields/currency.js
+++ b/client/src/views/fields/currency.js
@@ -26,7 +26,8 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
-define('views/fields/currency', ['views/fields/float'], function (Dep) {
+define('views/fields/currency', ['views/fields/float', 'ui/select'],
+function (Dep, /** module:ui/select*/Select) {
/**
* @class
@@ -39,23 +40,14 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
type: 'currency',
editTemplate: 'fields/currency/edit',
-
detailTemplate: 'fields/currency/detail',
-
detailTemplate1: 'fields/currency/detail-1',
-
detailTemplate2: 'fields/currency/detail-2',
-
detailTemplate3: 'fields/currency/detail-3',
-
listTemplate: 'fields/currency/list',
-
listTemplate1: 'fields/currency/list-1',
-
listTemplate2: 'fields/currency/list-2',
-
listTemplate3: 'fields/currency/list-3',
-
detailTemplateNoCurrency: 'fields/currency/detail-no-currency',
maxDecimalPlaces: 3,
@@ -64,7 +56,7 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
* @inheritDoc
*/
data: function () {
- var currencyValue = this.model.get(this.currencyFieldName) ||
+ let currencyValue = this.model.get(this.currencyFieldName) ||
this.getPreferences().get('defaultCurrency') ||
this.getConfig().get('defaultCurrency');
@@ -93,7 +85,7 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
this.isSingleCurrency = this.currencyList.length <= 1;
- var currencyValue = this.currencyValue = this.model.get(this.currencyFieldName) ||
+ let currencyValue = this.currencyValue = this.model.get(this.currencyFieldName) ||
this.defaultCurrency;
if (!~this.currencyList.indexOf(currencyValue)) {
@@ -107,10 +99,10 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
},
_getTemplateName: function () {
- if (this.mode === 'detail' || this.mode === 'list') {
+ if (this.mode === this.MODE_DETAIL || this.mode === this.MODE_LIST) {
var prop;
- if (this.mode === 'list') {
+ if (this.mode === this.MODE_LIST) {
prop = 'listTemplate' + this.getCurrencyFormat().toString();
}
else {
@@ -138,7 +130,7 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
},
formatNumberEdit: function (value) {
- var currencyDecimalPlaces = this.getConfig().get('currencyDecimalPlaces');
+ let currencyDecimalPlaces = this.getConfig().get('currencyDecimalPlaces');
if (value !== null) {
var parts = value.toString().split(".");
@@ -163,7 +155,7 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
formatNumberDetail: function (value) {
if (value !== null) {
- var currencyDecimalPlaces = this.getConfig().get('currencyDecimalPlaces');
+ let currencyDecimalPlaces = this.getConfig().get('currencyDecimalPlaces');
if (currencyDecimalPlaces === 0) {
value = Math.round(value);
@@ -179,7 +171,7 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
);
}
- var parts = value.toString().split(".");
+ let parts = value.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
@@ -187,7 +179,7 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
return parts[0];
}
else if (currencyDecimalPlaces) {
- var decimalPartLength = 0;
+ let decimalPartLength = 0;
if (parts.length > 1) {
decimalPartLength = parts[1].length;
@@ -196,9 +188,9 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
}
if (currencyDecimalPlaces && decimalPartLength < currencyDecimalPlaces) {
- var limit = currencyDecimalPlaces - decimalPartLength;
+ let limit = currencyDecimalPlaces - decimalPartLength;
- for (var i = 0; i < limit; i++) {
+ for (let i = 0; i < limit; i++) {
parts[1] += '0';
}
}
@@ -213,23 +205,25 @@ define('views/fields/currency', ['views/fields/float'], function (Dep) {
afterRender: function () {
Dep.prototype.afterRender.call(this);
- if (this.mode === 'edit') {
+ if (this.mode === this.MODE_EDIT) {
this.$currency = this.$el.find('[data-name="' + this.currencyFieldName + '"]');
this.$currency.on('change', () => {
this.model.set(this.currencyFieldName, this.$currency.val(), {ui: true});
});
+
+ Select.init(this.$currency);
}
},
fetch: function () {
- var value = this.$element.val();
+ let value = this.$element.val();
value = this.parse(value);
- var data = {};
+ let data = {};
- var currencyValue = this.$currency.val();
+ let currencyValue = this.$currency.val();
if (value === null) {
currencyValue = null;
diff --git a/client/src/views/fields/duration.js b/client/src/views/fields/duration.js
index 45f374b96c..1380a12237 100644
--- a/client/src/views/fields/duration.js
+++ b/client/src/views/fields/duration.js
@@ -26,9 +26,16 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
-define('views/fields/duration', ['views/fields/enum'], function (Dep) {
+define('views/fields/duration', ['views/fields/enum', 'ui/select'],
+function (Dep, /** module:ui/select*/Select) {
- return Dep.extend({
+ /**
+ * @class
+ * @name Class
+ * @memberOf module:views/fields/duration
+ * @extends module:views/fields/enum.Class
+ */
+ return Dep.extend(/** @lends module:views/fields/duration.Class# */{
type: 'duration',
@@ -39,10 +46,11 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
data: function () {
let valueIsSet = this.model.has(this.startField) && this.model.has(this.endField);
- return _.extend({
+ return {
valueIsSet: valueIsSet,
durationOptions: this.durationOptions,
- }, Dep.prototype.data.call(this));
+ ...Dep.prototype.data.call(this),
+ };
},
calculateSeconds: function () {
@@ -88,30 +96,39 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
this.listenTo(this, 'render', () => {
this.calculateSeconds();
- let durationOptions = '';
+ this.durationOptions = '';
- let options = this.defaultOptions = _.clone(this.model.getFieldParam(this.name, 'options'));
+ this.getOptions().forEach(d => {
+ let $o = $('
';
- });
-
- this.durationOptions = durationOptions;
-
this.stringValue = this.stringifyDuration(this.seconds);
});
},
+ /**
+ * @return {Number[]}
+ */
+ getOptions: function () {
+ let options = Espo.Utils.clone(this.model.getFieldParam(this.name, 'options') ?? []);
+
+ if (!this.model.get('isAllDay') && options.indexOf(this.seconds) === -1) {
+ options.push(this.seconds);
+ }
+
+ options.sort((a, b) => a - b);
+
+ return options;
+ },
+
setup: function () {
this.startField = this.model.getFieldParam(this.name, 'start');
this.endField = this.model.getFieldParam(this.name, 'end');
@@ -124,7 +141,7 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
this.blockDateEndChangeListener = false;
- this.listenTo(this.model, 'change:' + this.endField, () => {
+ this.listenTo(this.model, 'change:' + this.endField, (m, v, o) => {
if (this.blockDateEndChangeListener) {
return;
}
@@ -138,6 +155,10 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
this.seconds = moment(end).unix() - moment(start).unix();
+ if (o.updatedByDuration) {
+ return;
+ }
+
this.updateDuration();
});
@@ -205,11 +226,14 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
return parts.join(' ');
},
+ focusOnInlineEdit: function () {
+ Select.focus(this.$duration);
+ },
+
afterRender: function () {
let parentView = this.getParentView();
if (parentView && 'getView' in parentView) {
- this.startFieldView = parentView.getView(this.startField);
this.endFieldView = parentView.getView(this.endField);
}
@@ -220,8 +244,6 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
this.seconds = parseInt(this.$duration.val());
this.updateDateEnd();
-
- this.$duration.find('option.custom').remove();
});
let start = this.model.get(this.startField);
@@ -241,6 +263,8 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
}
}
}
+
+ Select.init(this.$duration, {});
}
},
@@ -311,49 +335,23 @@ define('views/fields/duration', ['views/fields/enum'], function (Dep) {
updateDuration: function () {
let seconds = this.seconds;
- if (seconds < 0) {
- if (this.mode === 'edit') {
- this.$duration.val('');
-
- return;
- }
-
- this.setup();
- this.render();
-
- return;
- }
-
if (this.isEditMode() && this.$duration && this.$duration.length) {
- this.$duration.find('option.custom').remove();
+ let options = this.getOptions().map(value => {
+ return {
+ value: value.toString(),
+ label: this.stringifyDuration(value),
+ };
+ });
- let $o = $('