YetiForceCompany/YetiForceCRM

View on GitHub
public_html/layouts/resources/Fields.js

Summary

Maintainability
F
1 mo
Test Coverage
/* {[The file is published on the basis of YetiForce Public License 6.5 that can be found in the following directory: licenses/LicenseEN.txt or yetiforce.com]} */
'use strict';

window.App.Fields = {
    Date: {
        months: [
            'JS_JAN',
            'JS_FEB',
            'JS_MAR',
            'JS_APR',
            'JS_MAY_SHORT',
            'JS_JUN',
            'JS_JUL',
            'JS_AUG',
            'JS_SEP',
            'JS_OCT',
            'JS_NOV',
            'JS_DEC'
        ],
        monthsTranslated: [
            'JS_JAN',
            'JS_FEB',
            'JS_MAR',
            'JS_APR',
            'JS_MAY_SHORT',
            'JS_JUN',
            'JS_JUL',
            'JS_AUG',
            'JS_SEP',
            'JS_OCT',
            'JS_NOV',
            'JS_DEC'
        ].map((monthName) => app.vtranslate(monthName)),
        fullMonths: [
            'JS_JANUARY',
            'JS_FEBRUARY',
            'JS_MARCH',
            'JS_APRIL',
            'JS_MAY',
            'JS_JUNE',
            'JS_JULY',
            'JS_AUGUST',
            'JS_SEPTEMBER',
            'JS_OCTOBER',
            'JS_NOVEMBER',
            'JS_DECEMBER'
        ],
        fullMonthsTranslated: [
            'JS_JANUARY',
            'JS_FEBRUARY',
            'JS_MARCH',
            'JS_APRIL',
            'JS_MAY',
            'JS_JUNE',
            'JS_JULY',
            'JS_AUGUST',
            'JS_SEPTEMBER',
            'JS_OCTOBER',
            'JS_NOVEMBER',
            'JS_DECEMBER'
        ].map((monthName) => app.vtranslate(monthName)),
        days: ['JS_SUN', 'JS_MON', 'JS_TUE', 'JS_WED', 'JS_THU', 'JS_FRI', 'JS_SAT'],
        daysTranslated: ['JS_SUN', 'JS_MON', 'JS_TUE', 'JS_WED', 'JS_THU', 'JS_FRI', 'JS_SAT'].map((monthName) =>
            app.vtranslate(monthName)
        ),
        fullDays: ['JS_SUNDAY', 'JS_MONDAY', 'JS_TUESDAY', 'JS_WEDNESDAY', 'JS_THURSDAY', 'JS_FRIDAY', 'JS_SATURDAY'],
        fullDaysTranslated: [
            'JS_SUNDAY',
            'JS_MONDAY',
            'JS_TUESDAY',
            'JS_WEDNESDAY',
            'JS_THURSDAY',
            'JS_FRIDAY',
            'JS_SATURDAY'
        ].map((monthName) => app.vtranslate(monthName)),

        /**
         * Register DatePicker
         * @param {$} parentElement
         * @param {boolean} registerForAddon
         * @param {object} customParams
         */
        register(parentElement, registerForAddon, customParams, className = 'dateField') {
            if (typeof parentElement === 'undefined') {
                parentElement = $('body');
            } else {
                parentElement = $(parentElement);
            }
            if (typeof registerForAddon === 'undefined') {
                registerForAddon = true;
            }
            let elements = $('.' + className, parentElement);
            if (parentElement.hasClass(className)) {
                elements = parentElement;
            }
            if (elements.length === 0) {
                return;
            }
            if (registerForAddon === true) {
                const parentDateElem = elements.closest('.date');
                $('.js-date__btn', parentDateElem).on('click', function inputGroupAddonClickHandler(e) {
                    // Using focus api of DOM instead of jQuery because show api of datePicker is calling e.preventDefault
                    // which is stopping from getting focus to input element
                    $(e.currentTarget)
                        .closest('.date')
                        .find('input.' + className)
                        .get(0)
                        .focus();
                });
            }
            let format = CONFIG.dateFormat;
            const elementDateFormat = elements.data('dateFormat');
            if (typeof elementDateFormat !== 'undefined') {
                format = elementDateFormat;
            }
            if (typeof $.fn.datepicker.dates[CONFIG.langKey] === 'undefined') {
                $.fn.datepicker.dates[CONFIG.langKey] = {
                    days: App.Fields.Date.fullDaysTranslated,
                    daysShort: App.Fields.Date.daysTranslated,
                    daysMin: App.Fields.Date.daysTranslated,
                    months: App.Fields.Date.fullMonthsTranslated,
                    monthsShort: App.Fields.Date.monthsTranslated,
                    today: app.vtranslate('JS_TODAY'),
                    clear: app.vtranslate('JS_CLEAR'),
                    format: format,
                    titleFormat: 'MM yyyy' /* Leverages same syntax as 'format' */,
                    weekStart: CONFIG.firstDayOfWeekNo
                };
            }
            let params = {
                todayBtn: 'linked',
                clearBtn: true,
                language: CONFIG.langKey,
                weekStart: CONFIG.firstDayOfWeekNo,
                autoclose: true,
                todayHighlight: true,
                format: format
            };
            if (typeof customParams !== 'undefined') {
                params = $.extend(params, customParams);
            }
            elements.each((_index, element) => {
                $(element).datepicker(
                    $.extend(
                        true,
                        Object.assign(params, { enableOnReadonly: !element.hasAttribute('readonly') }),
                        $(element).data('params')
                    )
                );
            });
            App.Fields.Utils.hideMobileKeyboard(elements);
            return elements;
        },

        /**
         * Register dateRangePicker
         * @param {jQuery} parentElement
         * @param {object} customParams
         */
        registerRange(parentElement, customParams = {}) {
            if (typeof parentElement === 'undefined') {
                parentElement = $('body');
            } else {
                parentElement = $(parentElement);
            }
            let elements = $('.dateRangeField', parentElement);
            if (parentElement.hasClass('dateRangeField')) {
                elements = parentElement;
            }
            if (elements.length === 0) {
                return;
            }
            let format = CONFIG.dateFormat.toUpperCase();
            const elementDateFormat = elements.data('dateFormat');
            if (typeof elementDateFormat !== 'undefined') {
                format = elementDateFormat.toUpperCase();
            }
            let ranges = {};
            ranges[app.vtranslate('JS_TODAY')] = [moment(), moment()];
            ranges[app.vtranslate('JS_TOMORROW')] = [moment().add(1, 'days'), moment().add(1, 'days')];
            ranges[app.vtranslate('JS_YESTERDAY')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
            ranges[app.vtranslate('JS_LAST_7_DAYS')] = [moment().subtract(6, 'days'), moment()];
            ranges[app.vtranslate('JS_NEXT_7_DAYS')] = [moment(), moment().add(6, 'days')];
            ranges[app.vtranslate('JS_CURRENT_MONTH')] = [moment().startOf('month'), moment().endOf('month')];
            ranges[app.vtranslate('JS_NEXT_MONTH')] = [
                moment().add(1, 'month').startOf('month'),
                moment().add(1, 'month').endOf('month')
            ];
            ranges[app.vtranslate('JS_LAST_MONTH')] = [
                moment().subtract(1, 'month').startOf('month'),
                moment().subtract(1, 'month').endOf('month')
            ];
            ranges[app.vtranslate('JS_NEXT_MONTH')] = [
                moment().add(1, 'month').startOf('month'),
                moment().add(1, 'month').endOf('month')
            ];
            ranges[app.vtranslate('JS_LAST_3_MONTHS')] = [
                moment().subtract(3, 'month').startOf('month'),
                moment().subtract(1, 'month').endOf('month')
            ];
            ranges[app.vtranslate('JS_NEXT_3_MONTHS')] = [moment().startOf('month'), moment().add(3, 'month').endOf('month')];
            ranges[app.vtranslate('JS_LAST_6_MONTHS')] = [
                moment().subtract(6, 'month').startOf('month'),
                moment().subtract(1, 'month').endOf('month')
            ];
            ranges[app.vtranslate('JS_NEXT_6_MONTHS')] = [moment().startOf('month'), moment().add(6, 'month').endOf('month')];

            let locale = App.Fields.DateTime.getDefaultLocale();
            locale.format = format;
            let params = {
                language: CONFIG.langKey,
                autoUpdateInput: false,
                autoApply: true,
                ranges: ranges,
                locale: locale
            };

            if (typeof customParams !== 'undefined') {
                params = $.extend(params, customParams);
            }
            parentElement
                .find('.js-date__btn')
                .off()
                .on('click', (e) => {
                    $(e.currentTarget).parent().next('.dateRangeField')[0].focus();
                });
            elements.each((_index, element) => {
                let el = $(element);
                let currentParams = $.extend(true, params, el.data('params'));
                el.daterangepicker(currentParams)
                    .on('apply.daterangepicker', function (_ev, picker) {
                        $(this).val(
                            picker.startDate.format(currentParams.locale.format) +
                                ',' +
                                picker.endDate.format(currentParams.locale.format)
                        );
                        $(this).trigger('change');
                    })
                    .on('show.daterangepicker', (ev, picker) => {
                        App.Fields.Utils.positionPicker(ev, picker);
                    })
                    .on('showCalendar.daterangepicker', (ev, picker) => {
                        App.Fields.Utils.positionPicker(ev, picker);
                        picker.container.addClass('js-visible');
                    })
                    .on('hide.daterangepicker', (_ev, picker) => {
                        picker.container.removeClass('js-visible');
                    });
                App.Fields.Utils.registerMobileDateRangePicker(el);
            });
        },
        /**
         * Function to get Date Instance
         * @param {string} dateTime
         * @param {string} dateFormat user date format
         * @returns {Date}
         */
        getDateInstance: function (dateTime, dateFormat = CONFIG.dateFormat) {
            let dateTimeComponents = dateTime.split(' '),
                dateComponent = dateTimeComponents[0],
                timeComponent = dateTimeComponents[1],
                seconds = '00',
                dotMode = '-';
            if (dateFormat.indexOf('.') !== -1) {
                dotMode = '.';
            } else if (dateFormat.indexOf('/') !== -1) {
                dotMode = '/';
            }
            let splittedDate = dateComponent.split(dotMode),
                splittedDateFormat = dateFormat.split(dotMode),
                year = splittedDate[splittedDateFormat.indexOf('yyyy')],
                month = splittedDate[splittedDateFormat.indexOf('mm')],
                day = splittedDate[splittedDateFormat.indexOf('dd')],
                dateInstance = Date.parse(year + '/' + month + '/' + day);

            if (isNaN(dateInstance) || year.length !== 4 || month.length > 2 || day.length > 2 || dateInstance == null) {
                throw app.vtranslate('JS_INVALID_DATE');
            }
            //Before creating date object time is set to 00
            //because as while calculating date object it depends system timezone
            if (typeof timeComponent === 'undefined') {
                timeComponent = '00:00:00';
            }
            let timeSections = timeComponent.split(':');
            if (typeof timeSections[2] !== 'undefined') {
                seconds = timeSections[2];
            }
            //Am/Pm component exits
            if (typeof dateTimeComponents[2] !== 'undefined') {
                if (dateTimeComponents[2].toLowerCase() === 'pm' && timeSections[0] !== '12') {
                    timeSections[0] = parseInt(timeSections[0], 10) + 12;
                }
                if (dateTimeComponents[2].toLowerCase() === 'am' && timeSections[0] === '12') {
                    timeSections[0] = '00';
                }
            }
            month = month - 1;
            return new Date(year, month, day, timeSections[0], timeSections[1], seconds);
        },
        /**
         * Format the Date object to a date in the format DB format, example: `2018-07-23`
         * @param {Date} date
         * @returns {string}
         */
        dateToDbFormat: function (date) {
            let d = date.getDate();
            let m = date.getMonth() + 1;
            let y = date.getFullYear();
            d = d <= 9 ? '0' + d : d;
            m = m <= 9 ? '0' + m : m;
            return y + '-' + m + '-' + d;
        },
        /**
         * Format the Date object to a date in the format user format, example: `2018/07/23`
         * @param {Date} date
         * @returns {string}
         */
        dateToUserFormat: function (date, format = CONFIG.dateFormat) {
            if (typeof date === 'string') {
                date = new Date(date);
            }
            let m = date.getMonth() + 1,
                d = date.getDate();
            d = d <= 9 ? '0' + d : d;
            m = m <= 9 ? '0' + m : m;
            return format.replace('yyyy', date.getFullYear()).replace('mm', m).replace('dd', d);
        },
        /**
         * Get last day of month
         * @param {integer} year
         * @param {integer} month
         * @returns {integer}
         */
        getLastMonthDay: function (year, month) {
            let date = new Date(year, month, 0);
            return date.getDate();
        },
        /**
         * Get number of days from a given date to now
         * @param {Date} dateTime
         * @returns {integer}
         */
        howManyDaysFromDate: function (dateTime) {
            let today = new Date();
            let toTime = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
            return Math.floor((toTime - dateTime.getTime()) / (1000 * 60 * 60 * 24)) + 1;
        },
        /**
         * Converting the date format to the format supported in the DatePicker, example: `yyyy-mm-dd` >> `Y-m-d`
         * @param {string} dateFormat
         * @returns {string}
         */
        convertToDatePickerFormat: function (dateFormat) {
            switch (dateFormat) {
                case 'yyyy-mm-dd':
                    return 'Y-m-d';
                case 'mm-dd-yyyy':
                    return 'm-d-Y';
                case 'dd-mm-yyyy':
                    return 'd-m-Y';
                case 'yyyy.mm.dd':
                    return 'Y.m.d';
                case 'mm.dd.yyyy':
                    return 'm.d.Y';
                case 'dd.mm.yyyy':
                    return 'd.m.Y';
                case 'yyyy/mm/dd':
                    return 'Y/m/d';
                case 'mm/dd/yyyy':
                    return 'm/d/Y';
                case 'dd/mm/yyyy':
                    return 'd/m/Y';
            }
        }
    },
    DateTime: class DateTime {
        constructor(container, params) {
            this.container = container;
            this.init(params);
        }
        /**
         * Register function
         * @param {jQuery} container
         * @param {Object} params
         */
        static register(container, params) {
            if (typeof container === 'undefined') {
                container = $('body');
            }
            if (container.hasClass('dateTimePickerField') && !container.prop('disabled')) {
                return new DateTime(container, params);
            }
            const instances = [];
            container.find('.dateTimePickerField:not([disabled])').each((_, e) => {
                let element = $(e);
                instances.push(new DateTime(element, params));
            });
            return instances;
        }
        /**
         * Format the Date object to a date in the format user format, example: `2018/07/23 03:00`
         * @param {Date} dateTime Date object
         * @returns {string} `2018/07/23 03:00`
         */
        static dateToUserFormat(dateTime, format = CONFIG.dateFormat) {
            format = format.toUpperCase();
            if (CONFIG.hourFormat == 24) {
                format += ' HH:mm';
            } else {
                format += ' hh:mm A';
            }
            return moment(dateTime).format(format);
        }
        /**
         * Gets default locale data
         * @returns {Object}
         */
        static getDefaultLocale() {
            if (!this.locale) {
                this.locale = {
                    separator: ',',
                    applyLabel: app.vtranslate('JS_APPLY'),
                    cancelLabel: app.vtranslate('JS_CANCEL'),
                    fromLabel: app.vtranslate('JS_FROM'),
                    toLabel: app.vtranslate('JS_TO'),
                    customRangeLabel: app.vtranslate('JS_CUSTOM'),
                    weekLabel: app.vtranslate('JS_WEEK').substr(0, 1),
                    firstDay: CONFIG.firstDayOfWeekNo,
                    daysOfWeek: App.Fields.Date.daysTranslated,
                    monthNames: App.Fields.Date.fullMonthsTranslated
                };
            }
            return { ...this.locale };
        }

        /**
         * Initialization datetime
         */
        init(customParams) {
            $('.input-group-text', this.container.closest('.dateTime')).on('click', function (e) {
                $(e.currentTarget).closest('.dateTime').find('input.dateTimePickerField').get(0).focus();
            });
            let dateFormat = CONFIG.dateFormat.toUpperCase();
            const elementDateFormat = this.container.data('dateFormat');
            if (typeof elementDateFormat !== 'undefined') {
                dateFormat = elementDateFormat.toUpperCase();
            }
            let hourFormat = CONFIG.hourFormat;
            const elementHourFormat = this.container.data('hourFormat');
            if (typeof elementHourFormat !== 'undefined') {
                hourFormat = elementHourFormat;
            }
            let timePicker24Hour = true;
            let timeFormat = 'HH:mm';
            if (hourFormat != '24') {
                timePicker24Hour = false;
                timeFormat = 'hh:mm A';
            }
            const format = dateFormat + ' ' + timeFormat;
            let isDateRangePicker = this.container.data('calendarType') !== 'range';
            let locale = App.Fields.DateTime.getDefaultLocale();
            locale.format = format;
            let params = {
                language: CONFIG.langKey,
                parentEl: this.container.closest('.dateTime'),
                singleDatePicker: isDateRangePicker,
                showDropdowns: true,
                timePicker: true,
                autoUpdateInput: false,
                timePicker24Hour: timePicker24Hour,
                timePickerIncrement: 1,
                autoApply: true,
                opens: 'left',
                locale: locale
            };
            if (typeof customParams !== 'undefined') {
                params = $.extend(params, customParams);
            }
            this.container
                .daterangepicker(params)
                .on('apply.daterangepicker', function applyDateRangePickerHandler(_ev, picker) {
                    if (isDateRangePicker) {
                        $(this).val(picker.startDate.format(format));
                    } else {
                        $(this).val(picker.startDate.format(format) + ',' + picker.endDate.format(format));
                    }
                })
                .on('showCalendar.daterangepicker', (ev, picker) => {
                    App.Fields.Utils.positionPicker(ev, picker);
                    picker.container.addClass('js-visible');
                });
            App.Fields.Utils.registerMobileDateRangePicker(this.container);
        }
    },
    Time: {
        /**
         * Format the Date object to a date in the format user format, example: `2018/07/23`
         * @param {Date} date
         * @returns {string}
         */
        dateToUserFormat: function (date, timeFormat) {
            if (typeof date === 'string') {
                date = new Date(date);
            }
            if (!timeFormat) {
                if (CONFIG.hourFormat == 24) {
                    timeFormat = 'HH:mm';
                } else {
                    timeFormat = 'hh:mm A';
                }
            }
            return moment(date).format(timeFormat);
        }
    },
    Colors: {
        /**
         * Function to check whether the color is dark or light
         */
        getColorContrast: function (hexcolor) {
            var r = parseInt(hexcolor.substr(0, 2), 16);
            var g = parseInt(hexcolor.substr(2, 2), 16);
            var b = parseInt(hexcolor.substr(4, 2), 16);
            var yiq = (r * 299 + g * 587 + b * 114) / 1000;
            return yiq >= 128 ? 'light' : 'dark';
        },
        getRandomColor: function () {
            var letters = '0123456789ABCDEF'.split('');
            var color = '#';
            for (var i = 0; i < 6; i++) {
                color += letters[Math.floor(Math.random() * 16)];
            }
            return color;
        },
        getRandomColors: function (count) {
            const colors = [];
            for (var i = 0; i < count; i++) {
                colors.push(this.getRandomColor());
            }
            return colors;
        },
        showPicker({ color, fieldToUpdate, bgToUpdate, cb }) {
            let registerPickerEvents = (modalContainer) => {
                let picker = window.ColorPicker.mount({
                    el: modalContainer.find('.js-color-picker')[0],
                    currentColor: color
                });
                modalContainer.find('.js-modal__save').on('click', (_) => {
                    let newColor = picker.getColor().hex;
                    cb && cb(newColor);
                    bgToUpdate && bgToUpdate.css('background', newColor);
                    fieldToUpdate && fieldToUpdate.val(newColor);
                    app.hideModalWindow(false, modalContainer.closest('.js-modal-container')[0].id);
                });
            };
            let url = `index.php?module=AppComponents&view=ColorPickerModal${color ? '&color=' + color : ''}`;
            app.showModalWindow({ url, cb: registerPickerEvents.bind(this) });
        }
    },
    Text: {
        /**
         * Register clip
         * @param {HTMLElement|jQuery} container
         * @param {string} key
         * @returns {ClipboardJS|undefined}
         */
        registerCopyClipboard: function (container, key = '.clipboard') {
            if (typeof container !== 'object' || $(container).length === 0) {
                return;
            }
            container = $(container).get(0);
            let elements = container.querySelectorAll(key);
            if (elements.length === 0) {
                elements = key;
                container = '';
            }
            return new ClipboardJS(elements, {
                container: container,
                text: function (trigger) {
                    app.showNotify({
                        text: app.vtranslate('JS_NOTIFY_COPY_TEXT'),
                        type: 'success'
                    });
                    trigger = $(trigger);
                    const element = $(trigger.data('copyTarget'), container);
                    let val;
                    if (typeof trigger.data('copyType') !== 'undefined') {
                        if (element.is('select')) {
                            val = element.find('option:selected').data(trigger.data('copyType'));
                        } else {
                            val = element.data(trigger.data('copyType'));
                        }
                    } else if (typeof trigger.data('copy-attribute') !== 'undefined') {
                        val = trigger.data(trigger.data('copy-attribute'));
                    } else {
                        val = element.val();
                    }
                    return val;
                }
            });
        },
        Editor: class {
            static initialization = false;
            constructor(container, params) {
                if (window.App.Fields.Text.Editor.initialization === false) {
                    CKEDITOR.disableAutoInline = true;
                    CKEDITOR.plugins.addExternal(
                        'base64image',
                        app.getMainParams('siteUrl') + 'layouts/resources/libraries/ckeditor/base64image/'
                    );
                    window.App.Fields.Text.Editor.initialization = true;
                }
                this.container = container;
                this.init(container, params);
            }
            /**
             * Register function
             * @param {jQuery} container
             * @param {Object} params
             */
            static register(container, params) {
                if (typeof container === 'undefined') {
                    container = $('body');
                }
                if (container.hasClass('js-editor') && !container.prop('disabled')) {
                    return new App.Fields.Text.Editor(container, $.extend(params, container.data()));
                }
                const instances = [];
                container.find('.js-editor:not([disabled])').each((_, e) => {
                    let element = $(e);
                    instances.push(new App.Fields.Text.Editor(element, $.extend(params, element.data())));
                });
                return instances;
            }
            /**
             * Initiation
             * @param {jQuery} element
             * @param {Object} params
             */
            init(element, params) {
                let config = {};
                if (element.hasClass('js-editor--basic')) {
                    config.toolbar = 'Min';
                }
                if (element.data('height')) {
                    config.height = element.data('height');
                }
                params = $.extend(config, params);
                this.isModal = element.closest('.js-modal-container').length;
                if (this.isModal && element.is(':visible')) {
                    let self = this;
                    this.progressInstance = $.progressIndicator({
                        blockInfo: {
                            enabled: true,
                            onBlock: () => {
                                self.loadEditor(element, params);
                            }
                        }
                    });
                } else {
                    App.Fields.Text.destroyEditor(element);
                    this.loadEditor(element, params);
                }
            }

            /*
             *Function to set the textArea element
             */
            setElement(element) {
                this.element = $(element);
                return this;
            }

            /*
             *Function to get the textArea element
             */
            getElement() {
                return this.element;
            }

            /*
             * Function to return Element's id atrribute value
             */
            getElementId() {
                return this.getElement().attr('id');
            }

            /*
             * Function to get the instance of ckeditor
             */
            getEditorInstanceFromName() {
                return CKEDITOR.instances[this.getElementId()];
            }

            /*
             * Function to load CkEditor
             * @param {HTMLElement|jQuery} element on which CkEditor has to be loaded
             * @param {Object} customConfig custom configurations for ckeditor
             */
            loadEditor(element, customConfig) {
                this.setElement(element);
                const instance = this.getEditorInstanceFromName();
                let config = {
                    language: CONFIG.langKey,
                    allowedContent: true,
                    disableNativeSpellChecker: false,
                    extraAllowedContent: 'div{page-break-after*}',
                    format_tags: 'p;h1;h2;h3;h4;h5;h6;pre;address;div',
                    removeButtons: '',
                    enterMode: CKEDITOR.ENTER_BR,
                    shiftEnterMode: CKEDITOR.ENTER_P,
                    emojiEnabled: false,
                    mentionsEnabled: false,
                    clipboard_handleImages: false,
                    on: {
                        instanceReady: (evt) => {
                            evt.editor.on('blur', function () {
                                evt.editor.updateElement();
                            });
                            if (this.isModal && this.progressInstance) {
                                this.progressInstance.progressIndicator({ mode: 'hide' });
                            }
                        },
                        beforeCommandExec: (e) => {
                            if (e.editor.mode === 'source') {
                                return this.validate(element, e);
                            }
                        }
                    },
                    removePlugins: 'scayt',
                    extraPlugins:
                        'colorbutton,pagebreak,colordialog,find,selectall,showblocks,div,print,font,justify,bidi,base64image',
                    toolbar: 'Full',
                    toolbar_Full: [
                        {
                            name: 'clipboard',
                            items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']
                        },
                        { name: 'editing', items: ['Find', 'Replace', '-', 'SelectAll'] },
                        { name: 'links', items: ['Link', 'Unlink'] },
                        {
                            name: 'insert',
                            items: ['base64image', 'Table', 'HorizontalRule', 'SpecialChar', 'PageBreak']
                        },
                        { name: 'tools', items: ['Maximize', 'ShowBlocks'] },
                        { name: 'paragraph', items: ['Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv'] },
                        { name: 'document', items: ['Source', 'Print'] },
                        '/',
                        { name: 'styles', items: ['Styles', 'Format', 'Font', 'FontSize'] },
                        {
                            name: 'basicstyles',
                            items: ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript']
                        },
                        { name: 'colors', items: ['TextColor', 'BGColor'] },
                        {
                            name: 'paragraph',
                            items: [
                                'NumberedList',
                                'BulletedList',
                                '-',
                                'JustifyLeft',
                                'JustifyCenter',
                                'JustifyRight',
                                'JustifyBlock',
                                '-',
                                'BidiLtr',
                                'BidiRtl'
                            ]
                        },
                        { name: 'basicstyles', items: ['CopyFormatting', 'RemoveFormat'] }
                    ],
                    toolbar_Min: [
                        {
                            name: 'basicstyles',
                            items: ['Bold', 'Italic', 'Underline', 'Strike']
                        },
                        { name: 'colors', items: ['TextColor', 'BGColor'] },
                        { name: 'tools', items: ['Maximize'] },
                        {
                            name: 'paragraph',
                            items: [
                                'NumberedList',
                                'BulletedList',
                                '-',
                                'JustifyLeft',
                                'JustifyCenter',
                                'JustifyRight',
                                'JustifyBlock',
                                '-',
                                'BidiLtr',
                                'BidiRtl'
                            ]
                        },
                        { name: 'basicstyles', items: ['CopyFormatting', 'RemoveFormat', 'Source'] }
                    ],
                    toolbar_Micro: [
                        {
                            name: 'basicstyles',
                            items: ['Bold', 'Italic', 'Underline', 'Strike']
                        },
                        { name: 'colors', items: ['TextColor', 'BGColor'] },
                        {
                            name: 'paragraph',
                            items: ['NumberedList', 'BulletedList', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock']
                        },
                        { name: 'basicstyles', items: ['CopyFormatting', 'RemoveFormat'] }
                    ],
                    toolbar_Clipboard: [
                        { name: 'document', items: ['Print'] },
                        { name: 'basicstyles', items: ['CopyFormatting', 'RemoveFormat'] },
                        {
                            name: 'clipboard',
                            items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']
                        }
                    ],
                    toolbar_PDF: [
                        {
                            name: 'clipboard',
                            items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']
                        },
                        { name: 'editing', items: ['Find', 'Replace', '-', 'SelectAll', '-'] },
                        { name: 'links', items: ['Link', 'Unlink'] },
                        {
                            name: 'insert',
                            items: ['base64image', 'Table', 'HorizontalRule', 'PageBreak']
                        },
                        { name: 'tools', items: ['Maximize', 'ShowBlocks'] },
                        { name: 'document', items: ['Source'] },
                        '/',
                        { name: 'styles', items: ['Styles', 'Format', 'Font', 'FontSize'] },
                        {
                            name: 'basicstyles',
                            items: ['Bold', 'Italic', 'Underline', 'Strike']
                        },
                        { name: 'colors', items: ['TextColor', 'BGColor'] },
                        {
                            name: 'paragraph',
                            items: ['JustifyLeft', 'JustifyCenter', 'JustifyRight']
                        },
                        { name: 'basicstyles', items: ['CopyFormatting', 'RemoveFormat'] }
                    ]
                };
                if (typeof customConfig !== 'undefined') {
                    config = $.extend(config, customConfig);
                }
                config = Object.assign(config, element.data());
                if (config.emojiEnabled) {
                    let emojiToolbar = { name: 'links', items: ['EmojiPanel'] };
                    if (typeof config.toolbar === 'string') {
                        config[`toolbar_${config.toolbar}`].push(emojiToolbar);
                    } else if (Array.isArray(config.toolbar)) {
                        config.toolbar.push(emojiToolbar);
                    }
                    config.extraPlugins = config.extraPlugins + ',emoji';
                    config.outputTemplate = '{id}';
                }
                if (config.mentionsEnabled) {
                    config.extraPlugins = config.extraPlugins + ',mentions';
                    config.mentions = this.registerMentions();
                }
                if (instance) {
                    CKEDITOR.remove(instance);
                }
                element.ckeditor(config);
            }

            /**
             * Register mentions
             * @returns {Array}
             */
            registerMentions() {
                let minSerchTextLength = app.getMainParams('gsMinLength');
                return [
                    {
                        feed: this.getMentionUsersData.bind(this),
                        itemTemplate: `<li data-id="{id}" class="row no-gutters">
                                            <div class="col-2 c-img__completion__container">
                                                <div class="{icon} m-auto u-w-fit u-fs-14px"></div>
                                                <img src="{image}" class="c-img__completion mr-2" alt="{label}" title="{label}">
                                            </div>
                                            <div class="col row-10 no-gutters u-overflow-x-hidden">
                                                <strong class="u-text-ellipsis--no-hover col-12">{label}</strong>
                                                <div class="fullname col-12 u-text-ellipsis--no-hover text-muted small">{category}</div>
                                            </div>
                                        </li>`,
                        outputTemplate: '<a href="#" data-id="@{id}" data-module="{module}">{label}</a>',
                        minChars: minSerchTextLength
                    },
                    {
                        feed: App.Fields.Text.getMentionData,
                        marker: '#',
                        pattern: /#[wа-я]{1,}|#\w{3,}$/,
                        itemTemplate: `<li data-id="{id}" class="row no-gutters">
                                            <div class="col-2 c-circle-icon">
                                                <span class="yfm-{module}"></span>
                                            </div>
                                            <div class="col-10 row no-gutters pl-1 u-overflow-x-hidden">
                                                <strong class="u-text-ellipsis--no-hover col-12">{label}</strong>
                                                <div class="fullname col-12 u-text-ellipsis--no-hover text-muted small">{category}</div>
                                            </div>
                                        </li>`,
                        outputTemplate: '<a href="#" data-id="#{id}" data-module="{module}">{label}</a>',
                        minChars: minSerchTextLength
                    }
                ];
            }

            /**
             * Get mention Users data (invoked by ck editor mentions plugin)
             * @param {object} opts
             * @param {function} callback
             */
            getMentionUsersData(opts, callback) {
                App.Fields.Text.getMentionData(opts, callback, 'owners');
            }

            /**
             * Function to validate the field value
             * @param {jQuery} element
             * @param {object} e
             */
            validate(element) {
                let status = true,
                    params;
                const form = element.closest('form');
                if (
                    element.data('purifyMode') ||
                    !form.find('[name="module"]').length ||
                    form.find('[name="parent"]').val() === 'Settings'
                ) {
                    params = {
                        module: 'Users',
                        action: 'Fields',
                        mode: 'validateByMode',
                        purifyMode: element.data('purifyMode') || 'Html',
                        value: element.val()
                    };
                } else {
                    params = {
                        module: element.closest('form').find('[name="module"]').val(),
                        action: 'Fields',
                        mode: 'validateForField',
                        fieldName: element.attr('name'),
                        fieldValue: element.val()
                    };
                }
                AppConnector.request({
                    async: false,
                    data: params
                })
                    .done(function (data) {
                        element.val(data.result.raw);
                    })
                    .fail(function () {
                        app.showNotify({
                            type: 'error',
                            text: app.vtranslate('JS_UNEXPECTED_ERROR')
                        });
                        status = false;
                    });
                return status;
            }
        },
        /**
         * Completions class for contenteditable html element for records, users and emojis. Params can be passed in data-completions- of contenteditable element or as argument. Default params:
         * {
                    completionsCollection: {
                        records: true,
                        users: true,
                        emojis: true
                    }
            }
         */
        Completions: class {
            /**
             * Constructor
             * @param {jQuery} inputDiv - contenteditable div
             * @param params
             */
            constructor(inputDiv = $('.js-completions').eq(0), params = {}) {
                if (typeof inputDiv === 'undefined' || inputDiv.length === 0) {
                    return;
                } else if (inputDiv.length === undefined) {
                    inputDiv = $(inputDiv);
                }
                let basicParams = {
                    completionsCollection: {
                        records: true,
                        users: true,
                        emojis: true
                    },
                    autolink: true
                };
                this.params = Object.assign(basicParams, inputDiv.data(), params);
                this.inputDiv = inputDiv;
                this.collection = [];
                if (this.params.completionsCollection.records) {
                    this.collection.push(this.registerMentionCollection('#'));
                }
                if (this.params.completionsCollection.users) {
                    this.collection.push(this.registerMentionCollection('@', 'owners'));
                }
                if (this.params.completionsCollection.emojis) {
                    this.collection.push(this.registerEmojiCollection());
                }
                this.register(inputDiv);
            }

            /**
             * Register mention collection for tribute.js
             * @param {string} symbol
             * @param {string} searchModule
             * @returns {{trigger: *, selectTemplate: selectTemplate, values: values, menuItemTemplate: (function(*): string), lookup: string, fillAttr: string}}
             */
            registerMentionCollection(symbol, searchModule = '-') {
                let self = this;
                return {
                    trigger: symbol,
                    selectTemplate: function (item) {
                        if (this.range.isContentEditable(this.current.element)) {
                            return `<a href="#" data-id="${symbol + item.original.id}" data-module="${
                                item.original.module
                            }">${item.original.label.split('(')[0].trim()}</a>`;
                        }
                        return symbol + item.original.label;
                    },
                    values: (text, cb) => {
                        if (text.length >= CONFIG.globalSearchAutocompleteMinLength) {
                            App.Fields.Text.getMentionData(text, (users) => cb(users), searchModule);
                        }
                    },
                    menuItemTemplate: function (item) {
                        return self.mentionTemplate({
                            id: item.original.id,
                            module: item.original.module,
                            category: item.original.category,
                            image: item.original.image,
                            label: item.original.label,
                            icon: item.original.icon
                        });
                    },
                    lookup: 'label',
                    fillAttr: 'label'
                };
            }

            /**
             * Register emoji collection for tribute.js
             * @returns {{trigger: string, selectTemplate: selectTemplate, menuItemTemplate: (function(*): string), lookup: string, fillAttr: string, values: Array}}
             */
            registerEmojiCollection() {
                return {
                    trigger: ':',
                    selectTemplate: function (item) {
                        if (this.range.isContentEditable(this.current.element)) {
                            return `<span data-id="${item.original.id}">${item.original.symbol}</span>`;
                        }
                        return item.original.symbol;
                    },
                    menuItemTemplate: function (item) {
                        return `<span data-id="${item.original.id}">${item.original.symbol} ${item.original.id}</span>`;
                    },
                    lookup: 'id',
                    fillAttr: 'keywords',
                    values: (text, cb) => {
                        if (text.length >= 2) {
                            cb(App.emoji);
                        }
                    }
                };
            }
            /*
             * Mention template
             */
            mentionTemplate(params) {
                let icon = '';
                if (params.module !== undefined) {
                    icon = `yfm-${params.module}`;
                }
                if (params.icon !== undefined && params.icon !== '') {
                    icon = params.icon;
                }
                let avatar = `<div class="col-2 c-circle-icon">
                                <span class="${icon}"></span>
                            </div>`;
                if (params.image !== undefined && params.image !== '') {
                    avatar = `<div class="col-2 c-img__completion__container m-0"><img src="${params.image}" class="c-img__completion" alt=${params.label}" title="${params.label}"></div>`;
                }
                return `<div data-id="${params.id}" class="row no-gutters">
                            ${avatar}
                            <div class="col-10 row no-gutters pl-1 u-overflow-x-hidden">
                                <strong class="u-text-ellipsis--no-hover col-12">${params.label}</strong>
                                <div class="fullname col-12 u-text-ellipsis--no-hover text-muted small">${params.category}</div>
                            </div>
                        </div>`;
            }
            /**
             * Auto link
             */
            autoLink() {
                let fillChar = '\u200B';
                let sel = window.getSelection(),
                    range = sel.getRangeAt(0).cloneRange(),
                    offset,
                    charCode,
                    getParentByTagName = function (node, tags) {
                        if (node && !isBody(node)) {
                            while (node) {
                                if (tags[node.tagName] || isBody(node)) {
                                    return !tags[node.tagName] && isBody(node) ? null : node;
                                }
                                node = node.parentNode;
                            }
                        }
                        return null;
                    },
                    isBody = function (node) {
                        return node && node.nodeType == 1 && node.tagName.toLowerCase() == 'body';
                    },
                    html = function (str) {
                        return str.replace(/&((g|l|quo)t|amp|#39);/g, function (m) {
                            return { '&lt;': '<', '&amp;': '&', '&quot;': '"', '&gt;': '>', '&#39;': "'" }[m];
                        });
                    },
                    isFillChar = function (node) {
                        return node.nodeType == 3 && !node.nodeValue.replace(new RegExp('' + fillChar), '').length;
                    };

                let start = range.startContainer;
                while (start.nodeType == 1 && range.startOffset > 0) {
                    start = range.startContainer.childNodes[range.startOffset - 1];
                    if (!start) break;
                    range.setStart(start, start.nodeType == 1 ? start.childNodes.length : start.nodeValue.length);
                    range.collapse(true);
                    start = range.startContainer;
                }
                do {
                    if (range.startOffset == 0) {
                        start = range.startContainer.previousSibling;
                        while (start && start.nodeType == 1) {
                            start = start.lastChild;
                        }
                        if (!start || isFillChar(start)) break;
                        offset = start.nodeValue.length;
                    } else {
                        start = range.startContainer;
                        offset = range.startOffset;
                    }
                    range.setStart(start, offset - 1);
                    charCode = range.toString().charCodeAt(0);
                } while (charCode != 160 && charCode != 32);
                if (
                    range
                        .toString()
                        .replace(new RegExp(fillChar, 'g'), '')
                        .match(/(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i)
                ) {
                    while (range.toString().length) {
                        if (/^(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i.test(range.toString())) break;
                        try {
                            range.setStart(range.startContainer, range.startOffset + 1);
                        } catch (e) {
                            let startCont = range.startContainer,
                                next;
                            while (!(next = startCont.nextSibling)) {
                                if (isBody(startCont)) return;
                                startCont = startCont.parentNode;
                            }
                            range.setStart(next, 0);
                        }
                    }
                    if (getParentByTagName(range.startContainer, { a: 1, A: 1 })) return;
                    let href = range
                            .toString()
                            .replace(/<[^>]+>/g, '')
                            .replace(new RegExp(fillChar, 'g'), ''),
                        hrefFull = /^(?:https?:\/\/)/gi.test(href) ? href : 'http://' + href,
                        url = new URL(hrefFull);
                    let allowedHosts = CONFIG.purifierAllowedDomains;
                    if (allowedHosts !== false && allowedHosts.indexOf(url.host) === -1) {
                        return;
                    }
                    let a = document.createElement('a'),
                        text = document.createTextNode(' ');
                    a.appendChild(range.extractContents());
                    a.innerHTML = href;
                    a.href = hrefFull ? html(hrefFull) : '';
                    a.setAttribute('rel', 'noopener noreferrer');
                    a.setAttribute('target', '_blank');

                    range.insertNode(a);
                    a.parentNode.insertBefore(text, a.nextSibling);
                    range.setStart(text.nextSibling, 0);
                    range.collapse(true);
                    sel.removeAllRanges();
                    sel.addRange(range);
                }
            }
            /**
             * Register
             * @param {jQuery} inputDiv - contenteditable div
             */
            register(inputDiv) {
                const self = this;
                this.completionsCollection = new Tribute({
                    collection: self.collection,
                    allowSpaces: true
                });
                this.completionsCollection.attach(inputDiv[0]);
                if (this.params.completionsTextarea !== undefined) {
                    this.registerCompletionsTextArea(inputDiv);
                }
                if (this.params.completionsButtons !== undefined) {
                    this.registerCompletionsButtons();
                }
                if (this.params.autolink) {
                    this.registerAutoLinker(inputDiv);
                }
                if (App.emoji === undefined) {
                    fetch(`${CONFIG.siteUrl}/vendor/ckeditor/ckeditor/plugins/emoji/emoji.json`)
                        .then((response) => response.json())
                        .then((response) => {
                            App.emoji = response;
                        })
                        .catch((error) => console.error('Error:', error));
                }
                this.registerTagClick(inputDiv);
            }

            /**
             * Register autolink
             * @param {jQuery} inputDiv - contenteditable div
             */
            registerAutoLinker(inputDiv) {
                inputDiv.on('keypress', (e) => {
                    if (e.keyCode === 32 || e.keyCode === 13) {
                        this.autoLink();
                    }
                });
            }

            /**
             * Register completons hidden textarea - useful with forms
             * @param {jQuery} inputDiv - contenteditable div
             */
            registerCompletionsTextArea(inputDiv) {
                let textarea = inputDiv.siblings(`[name=${inputDiv.attr('id')}]`);
                inputDiv
                    .on('focus', function () {
                        textarea.val(inputDiv.html());
                    })
                    .on('blur keyup paste input', function () {
                        textarea.val(inputDiv.html());
                    });
            }

            /**
             * Register tag click
             * @param inputDiv
             */
            registerTagClick(inputDiv) {
                inputDiv
                    .closest('.js-completions__container')
                    .find('.js-completions__messages')
                    .on('click', '.js-completions__tag', (e) => {
                        e.preventDefault();
                        inputDiv.append($(e.target).clone());
                    });
            }

            /**
             * Register completions buttons
             */
            registerCompletionsButtons() {
                let completionsContainer = this.inputDiv.parents().eq(3);
                completionsContainer.find('.js-completions__users').on('click', (e) => {
                    this.completionsCollection.showMenuForCollection(this.inputDiv[0], 1);
                });
                completionsContainer.find('.js-completions__records').on('click', (e) => {
                    this.completionsCollection.showMenuForCollection(this.inputDiv[0], 0);
                });
            }
        },

        /**
         * Get mention data (invoked by ck editor mentions plugin and tribute.js)
         * @param {object} opts
         * @param {function} callback
         * @param {string} searchModule
         */
        getMentionData(text, callback, searchModule = '-') {
            let basicSearch = new Vtiger_BasicSearch_Js();
            basicSearch.reduceNumberResults = app.getMainParams('gsAmountResponse');
            basicSearch.returnHtml = false;
            basicSearch.searchModule = searchModule;
            if (typeof text === 'object') {
                text = text.query.toLowerCase();
            }
            if (searchModule === 'owners') {
                AppConnector.request({
                    action: 'Search',
                    mode: 'owners',
                    value: text
                }).done((data) => {
                    callback(data.result);
                });
            } else {
                basicSearch.search(text).done(function (data) {
                    data = JSON.parse(data);
                    let serverDataFormat = data.result,
                        reponseDataList = [];
                    for (let id in serverDataFormat) {
                        let responseData = serverDataFormat[id];
                        reponseDataList.push(responseData);
                    }
                    callback(reponseDataList);
                });
            }
        },

        /**
         * Destroy ckEditor
         * @param {jQuery} element
         */
        destroyEditor(element) {
            if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances && element.attr('id') in CKEDITOR.instances) {
                CKEDITOR.instances[element.attr('id')].destroy();
            }
        },

        /**
         * Generate random character
         * @returns {string}
         */
        generateRandomChar() {
            const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZ';
            const rand = Math.floor(Math.random() * chars.length);
            return chars.substring(rand, rand + 1);
        },

        /**
         * generate random hash
         * @returns {string}
         */
        generateRandomHash(prefix = '') {
            prefix = prefix.toString();
            const hash =
                Math.random().toString(36).substr(2, 10) +
                Math.random().toString(36).substr(2, 10) +
                new Date().valueOf() +
                Math.random().toString(36).substr(2, 6);
            return prefix ? prefix + hash : hash;
        }
    },
    Picklist: {
        /**
         * Function which will convert ui of select boxes.
         * @params parent - select element
         * @params view - select2
         * @params viewParams - select2 params
         * @returns jquery object list which represents changed select elements
         */
        changeSelectElementView: function (parent, view, viewParams) {
            if (typeof parent === 'undefined') {
                parent = $('body');
            }
            if (typeof view === 'undefined') {
                const select2Elements = $('select.select2', parent).toArray();
                select2Elements.forEach((elem) => {
                    this.changeSelectElementView($(elem), 'select2', viewParams);
                });
                return;
            }
            //If view is select2, This will convert the ui of select boxes to select2 elements.
            if (view === 'select2') {
                return App.Fields.Picklist.showSelect2ElementView(parent, viewParams);
            } else {
                app.errorLog(new Error(`Unknown select type [${view}]`));
            }
        },
        /**
         * Function which will show the select2 element for select boxes . This will use select2 library
         */
        showSelect2ElementView(selectElement, params) {
            let self = this;
            selectElement = $(selectElement);
            if (typeof params === 'undefined') {
                params = {};
            }
            if ($(selectElement).length > 1) {
                return $(selectElement).each((_, element) => {
                    this.showSelect2ElementView($(element).eq(0), params);
                });
            }
            params = this.registerParams(selectElement, params);
            if (params.selectLazy && !selectElement.hasClass('js-lazy-select-active')) {
                return App.Fields.Picklist.showLazySelect(selectElement, {
                    lazyElements: app.getMainParams('picklistLimit'),
                    data: this.registerLazySelectOptions(selectElement),
                    selectParams: params
                });
            }
            const computeDropdownHeight = (e, dropdownContainer) => {
                setTimeout(() => {
                    if (!dropdownContainer.find('.select2-dropdown--above').length) {
                        const dropdownList = dropdownContainer.find('.select2-results > .select2-results__options');
                        const marginBottom = 35;
                        const selectOffsetTop = $(e.currentTarget).offset().top;
                        dropdownList.css({
                            'max-height':
                                $(window).height() - selectOffsetTop - marginBottom - (dropdownList.offset().top - selectOffsetTop)
                        });
                    }
                }, 100);
            };
            selectElement.each(function () {
                let select = $(this);
                let htmlBoolParams = select.data('select');
                if (htmlBoolParams === 'tags') {
                    params.tags = true;
                    params.tokenSeparators = [','];
                } else {
                    params[htmlBoolParams] = true;
                }
                select
                    .select2(params)
                    .on('select2:open', (e) => {
                        computeDropdownHeight(e, $('.select2-container--open:not(.select2-container--below)'));
                        if (select.data('unselecting')) {
                            select.removeData('unselecting');
                            setTimeout(function () {
                                select.each(function () {
                                    $(this).select2('close');
                                });
                            }, 1);
                        }
                        let instance = $(e.currentTarget).data('select2');
                        instance.$dropdown.css('z-index', 1000002);
                        /**
                         * Fix auto focusing in select2 with jQuery 3.6.0
                         * see: https://github.com/select2/select2/issues/5993
                         */
                        if (instance.dropdown.$search) {
                            instance.dropdown.$search.get(0).focus();
                        }
                    })
                    .on('select2:unselect', () => {
                        select.data('unselecting', true);
                    });
                if (typeof self[params.selectCb] === 'function') {
                    self[params.selectCb](select, params);
                }
            });
            return selectElement;
        },
        /**
         * Register params
         * @param selectElement
         * @param params
         * @returns {*}
         */
        registerParams(selectElement, params) {
            if (typeof params.dropdownParent === 'undefined') {
                const modalParent = $(selectElement).closest('.modal-body');
                if (modalParent.length) {
                    params.dropdownParent = modalParent;
                }
            }
            let data = selectElement.data();
            if (data != null) {
                params = $.extend(data, params);
            }
            params.language = {};
            params.theme = 'bootstrap';
            const width = $(selectElement).data('width');
            if (typeof width !== 'undefined') {
                params.width = width;
            } else {
                params.width = '100%';
            }
            params.containerCssClass = 'form-control w-100';
            const containerCssClass = selectElement.data('containerCssClass');
            if (typeof containerCssClass !== 'undefined') {
                params.containerCssClass += ' ' + containerCssClass;
            }
            params.language.noResults = function () {
                return app.vtranslate('JS_NO_RESULTS_FOUND');
            };
            params.language.removeAllItems = function () {
                return app.vtranslate('JS_REMOVE_ALL_ITEMS');
            };
            // Sort DOM nodes alphabetically in select box.
            if (typeof params['customSortOptGroup'] !== 'undefined' && params['customSortOptGroup']) {
                $('optgroup', selectElement).each(function () {
                    let optgroup = $(this);
                    let options = optgroup
                        .children()
                        .toArray()
                        .sort(function (a, b) {
                            var aText = $(a).text();
                            var bText = $(b).text();
                            return aText < bText ? 1 : -1;
                        });
                    $.each(options, function (i, v) {
                        optgroup.prepend(v);
                    });
                });
                delete params['customSortOptGroup'];
            }

            //formatSelectionTooBig param is not defined even it has the maximumSelectionLength,
            //then we should send our custom function for formatSelectionTooBig
            if (typeof params.maximumSelectionLength !== 'undefined' && typeof params.formatSelectionTooBig === 'undefined') {
                //custom function which will return the maximum selection size exceeds message.
                params.language.maximumSelected = function (limit) {
                    return app.vtranslate('JS_YOU_CAN_SELECT_ONLY') + ' ' + limit.maximum + ' ' + app.vtranslate('JS_ITEMS');
                };
            }
            if (typeof selectElement.attr('multiple') !== 'undefined' && !params.placeholder) {
                params.placeholder = app.vtranslate('JS_SELECT_SOME_OPTIONS');
            } else if (!params.placeholder) {
                params.placeholder = app.vtranslate('JS_SELECT_AN_OPTION');
            }
            if (typeof params.templateResult === 'undefined') {
                params.templateResult = function (data, container) {
                    if (data.element && data.element.className) {
                        $(container).addClass(data.element.className);
                    }
                    const resultContainer = document.createElement('span');
                    if (typeof data.name === 'undefined') {
                        resultContainer.innerText = data.text;
                    } else if (data.type == 'optgroup') {
                        const strong = document.createElement('strong');
                        strong.innerText = data.name;
                        strong.appendChild(document.createTextNode(file.name));
                        resultContainer.appendChild(strong);
                    } else {
                        resultContainer.innerText = data.name;
                    }
                    return resultContainer;
                };
            } else if (typeof this[params.templateResult] === 'function') {
                params.templateResult = this[params.templateResult];
            }
            if (typeof params.templateSelection === 'undefined') {
                params.templateSelection = function (item, container) {
                    if (item.element && item.element.className) {
                        $(container).addClass(item.element.className);
                    }
                    const span = document.createElement('span');
                    if (item.text === '') {
                        span.innerText = item.name;
                    } else {
                        span.innerText = item.text;
                    }
                    return span;
                };
            } else if (typeof this[params.templateSelection] === 'function') {
                params.templateSelection = this[params.templateSelection];
            }
            if (selectElement.data('ajaxSearch') === 1) {
                params = this.registerAjaxParams(selectElement, params);
            }
            return params;
        },
        /**
         * Register ajax params
         * @param {jQuery} selectElement
         * @param {Object} params
         * @returns {Object}
         */
        registerAjaxParams(selectElement, params) {
            params.tags = false;
            params.language.searching = function () {
                return app.vtranslate('JS_SEARCHING');
            };
            params.language.inputTooShort = function (args) {
                var remainingChars = args.minimum - args.input.length;
                return app.vtranslate('JS_INPUT_TOO_SHORT').replace('_LENGTH_', remainingChars);
            };
            params.language.errorLoading = function () {
                return app.vtranslate('JS_NO_RESULTS_FOUND');
            };
            params.placeholder = '';
            params.ajax = {
                url: selectElement.data('ajaxUrl'),
                dataType: 'json',
                delay: 250,
                method: 'POST',
                data:
                    params['ajax'] && params['ajax']['data']
                        ? params['ajax']['data']
                        : function (item) {
                                return {
                                    value: item.term, // search term
                                    page: item.page
                                };
                          },
                processResults:
                    params['ajax'] && params['ajax']['processResults']
                        ? params['ajax']['processResults']
                        : function (data, _params) {
                                var items = new Array();
                                if (data.success == true) {
                                    selectElement.find('option').each(function () {
                                        var currentTarget = $(this);
                                        items.push({
                                            label: currentTarget.html(),
                                            value: currentTarget.val()
                                        });
                                    });
                                    items = items.concat(data.result.items);
                                }
                                return {
                                    results: items,
                                    pagination: {
                                        more: false
                                    }
                                };
                          },
                cache: false
            };
            params.escapeMarkup = function (markup) {
                if (markup !== 'undefined') return markup;
            };
            params.minimumInputLength = 3;
            if (selectElement.data('minimumInput') !== 'undefined') {
                params.minimumInputLength = selectElement.data('minimumInput');
            }
            params.templateResult = function (data) {
                if (typeof data.name === 'undefined') {
                    return data.text;
                }
                if (data.type == 'optgroup') {
                    return '<strong>' + data.name + '</strong>';
                } else {
                    return '<span>' + data.name + '</span>';
                }
            };
            params.templateSelection = function (data, _container) {
                const span = document.createElement('span');
                if (data.text === '') {
                    span.innerText = data.name;
                } else {
                    span.innerText = data.text;
                }
                return span;
            };
            return params;
        },
        /**
         * Prepend template with a flag, function is called select2
         * @param optionData
         * @returns {Mixed|jQuery|HTMLElement}
         */
        prependDataTemplate(optionData) {
            const span = document.createElement('span');
            if (optionData.id !== undefined && optionData.id !== '') {
                span.innerHTML = optionData.element.dataset.template;
                if (optionData.element.dataset.state !== undefined) {
                    const icon = span.querySelector('.js-select-option-event');
                    //check if element has icons with different states
                    if (optionData.element.dataset.state === 'active') {
                        icon.classList.replace(optionData.element.dataset.iconInactive, optionData.element.dataset.iconActive);
                    } else {
                        icon.classList.replace(optionData.element.dataset.iconActive, optionData.element.dataset.iconInactive);
                    }
                }
            } else {
                span.innerText = optionData.text;
            }
            return span;
        },
        /**
         * Register select sortable
         * @param select
         * @param params
         */
        registerSelectSortable(select, params) {
            this.sortSelectOptions(select);
            this.registerSortEvent(select, params.sortableCb);
        },
        /**
         * Sort elements (options) in select by data-sort-index
         * @param {jQuery} select2 element
         */
        sortSelectOptions(select) {
            select
                .find('option[data-sort-index]')
                .sort((a, b) => {
                    return $(b).data('sort-index') < $(a).data('sort-index') ? 1 : -1;
                })
                .appendTo(select);
        },
        /**
         * Register select drag and drop sorting
         * @param {jQuery} select2 element
         * @param {function} callback function
         */
        registerSortEvent(select, cb = () => {}) {
            let ul = select.next('.select2-container').first('ul.select2-selection__rendered');
            ul.sortable({
                items: 'li:not(.select2-search__field)',
                tolerance: 'pointer',
                stop: function () {
                    $(ul.find('.select2-selection__choice').get().reverse()).each(function () {
                        let optionTitle = $(this).attr('title');
                        select.find('option').each(function () {
                            if ($(this).text() === optionTitle) {
                                select.prepend($(this));
                            }
                        });
                    });
                    cb(select);
                    select.trigger('sortable:change');
                }
            });
        },
        /**
         * Register icons events in select2 options
         * @param selectElement
         */
        registerIconsEvents(selectElement) {
            selectElement.on('select2:selecting', (event) => {
                let currentTarget = $(event.params.args.originalEvent.target);
                if (!currentTarget.hasClass('js-select-option-event') && !currentTarget.is('path')) {
                    return;
                }
                event.preventDefault();
                if (currentTarget.is('path')) {
                    //svg target fix
                    currentTarget = currentTarget.closest('.js-select-option-event');
                }
                let currentElementData = $(event.params.args.data.element).data(),
                    optionElement = $(event.params.args.data.element),
                    progressIndicatorElement = $.progressIndicator({ blockInfo: { enabled: true } });
                AppConnector.request(currentElementData.url)
                    .done((data) => {
                        progressIndicatorElement.progressIndicator({ mode: 'hide' });
                        let response = data.result;
                        const icon = currentTarget.get(0);
                        //check if element has icons with different states
                        if (response && response.result) {
                            if (optionElement.attr('data-state') === 'active') {
                                optionElement.attr('data-state', 'inactive');
                                icon.classList.replace(currentElementData.iconActive, currentElementData.iconInactive);
                            } else {
                                optionElement.attr('data-state', 'active');
                                icon.classList.replace(currentElementData.iconInactive, currentElementData.iconActive);
                            }
                            if (response.message) {
                                app.showNotify({ text: response.message, type: 'success' });
                            }
                        } else if (response && response.message) {
                            app.showNotify({
                                text: response.message,
                                type: 'error'
                            });
                        }
                    })
                    .fail(function () {
                        progressIndicatorElement.progressIndicator({ mode: 'hide' });
                    });
            });
        },
        /**
         * Show lazy select based on data passed in js.
         *
         * @param   {object}  selectElement  jQuery
         * @param   {object}  params         contains selectParams object, lazyElements number, data array
         */
        showLazySelect(selectElement, params) {
            $.fn.select2.amd.require(['select2/data/array', 'select2/utils'], (ArrayData, Utils) => {
                function CustomData($element, params) {
                    CustomData.__super__.constructor.call(this, $element, params);
                }
                Utils.Extend(CustomData, ArrayData);
                CustomData.prototype.query = (options, callback) => {
                    let results = [];
                    if (options.term && options.term !== '') {
                        results = params.data.filter((e) => {
                            return e.text.toUpperCase().indexOf(options.term.toUpperCase()) >= 0;
                        });
                    } else {
                        results = params.data;
                    }
                    if (!('page' in options)) {
                        options.page = 1;
                    }
                    let data = {};
                    data.results = results.slice((options.page - 1) * params.lazyElements, options.page * params.lazyElements);
                    data.pagination = {};
                    data.pagination.more = options.page * params.lazyElements < results.length;
                    callback(data);
                };
                params.selectParams = Object.assign(params.selectParams, {
                    ajax: {},
                    dataAdapter: CustomData
                });
                selectElement.addClass('js-lazy-select-active');
                this.showSelect2ElementView(selectElement, params.selectParams);
                let selectedOption = selectElement.data('selected-value');
                if (selectedOption) {
                    let text = selectedOption;
                    if (
                        selectElement.data('fieldinfo').picklistvalues.hasOwnProperty(selectedOption) &&
                        !selectElement.get(0).dataset.templateResult
                    ) {
                        text = selectElement.data('fieldinfo').picklistvalues[selectedOption];
                    }
                    this.createSelectedOption(selectElement, text, selectedOption);
                }
            });
        },
        /**
         * Register lazy select options
         *
         * @param   {object}  selectElement  [selectElement description]
         *
         * @return  {object}                 [return description]
         */
        registerLazySelectOptions(selectElement) {
            let options = [];
            if (
                selectElement.data('fieldinfo') &&
                selectElement.data('fieldinfo').picklistvalues &&
                !selectElement.get(0).dataset.templateResult
            ) {
                options = $.map(selectElement.data('fieldinfo').picklistvalues, function (val, key) {
                    return { id: key, text: val };
                });
            } else {
                options = $.map(selectElement.find('option'), (item) => {
                    return {
                        id: item.value,
                        element: item,
                        text: item.text,
                        selected: item.selected,
                        disabled: item.disabled
                    };
                });
            }
            return options;
        },
        /**
         * Set a value for the field
         *
         * @param   {jQuery}  field Field element
         * @param   {mixed}  value The value to set
         * @param   {object}  params Additional parameters [optional]
         *
         * @return  {mixed} The value that has been set
         */
        setValue(field, value, params) {
            let type = 'value';
            if (params && params['type']) {
                type = params['type'];
            }
            const option = this.findOption(field, value, type);
            if (!option) {
                return false;
            }
            if (field.hasClass('js-lazy-select-active')) {
                this.createSelectedOption(field, option.text, option.value);
            } else {
                field.val(option.value).trigger('change');
            }
            return option.value;
        },
        /**
         * Find option.
         *
         * @param   {object}  selectElement  [selectElement description]
         * @param   {string}  searchValue
         * @param   {string}  type           value|text|all
         *
         * @return  {boolean|object}         false or option object
         */
        findOption(selectElement, searchValue, type = 'value') {
            let foundOption = false;
            const selectValues = this.getSelectOptions(selectElement);
            const getFieldValueFromText = () => Object.keys(selectValues).find((key) => selectValues[key] === searchValue);
            const valueExists = () => selectValues.hasOwnProperty(searchValue);
            const createOption = () => {
                return { text: selectValues[foundOption], value: foundOption };
            };
            switch (type) {
                case 'value':
                    if (valueExists()) {
                        foundOption = searchValue;
                    }
                    break;
                case 'text':
                    foundOption = getFieldValueFromText();
                    break;
                case 'all':
                    if (valueExists()) {
                        foundOption = searchValue;
                    } else {
                        foundOption = getFieldValueFromText();
                    }
                    break;
            }
            return foundOption ? createOption() : false;
        },
        /**
         * Get select options
         *
         * @param   {object}  selectElement  jQuery
         *
         * @return  {object}                 [return description]
         */
        getSelectOptions(selectElement) {
            if (selectElement.data('fieldinfo') && selectElement.data('fieldinfo').picklistvalues) {
                return selectElement.data('fieldinfo').picklistvalues;
            } else {
                let optionsObject = {};
                selectElement.find('option').each((_i, element) => {
                    optionsObject[element.value] = element.text;
                });
                return optionsObject;
            }
        },
        /**
         * Create selected option
         *
         * @param   {object}  selectElement  jQuery
         * @param   {string}  text
         * @param   {string}  value
         */
        createSelectedOption(selectElement, text, value) {
            const newOption = new Option(text, value, true, true);
            selectElement.append(newOption).trigger('change');
        }
    },
    MultiImage: {
        currentFileUploads: 0,
        register(container) {
            $('.js-multi-image', container).each(function () {
                new MultiImage($(this));
            });
        }
    },
    MultiEmail: {
        register($container) {
            $('.js-multi-email', $container).each((idx, multiEmailField) => {
                let $multiEmailField = $(multiEmailField);
                $multiEmailField.on('change', '.js-multi-email', (e) => {
                    App.Fields.MultiEmail.parseToJSON($multiEmailField);
                });
                $multiEmailField.on('click', '.js-multi-email-consenticon', (e) => {
                    App.Fields.MultiEmail.toggleConsent($(e.target));
                    App.Fields.MultiEmail.parseToJSON($multiEmailField);
                });
                $multiEmailField.on('click', '.js-multi-email-add', (e) => {
                    App.Fields.MultiEmail.addItem($multiEmailField);
                });
                $multiEmailField.on('click', '.js-multi-email-remove', (e) => {
                    App.Fields.MultiEmail.removeItem($(e.target));
                    App.Fields.MultiEmail.parseToJSON($multiEmailField);
                });
            });
        },
        /**
         * Converts data to json and set MultiEmail field value
         * @param $multiEmailField
         */
        parseToJSON($multiEmailField) {
            let value = [];
            $('.js-multi-email-item', $multiEmailField).each((idx, item) => {
                let $item = $(item);
                let email = $('.js-multi-email', $item).val();
                let consent = $('.js-multi-email-consent', $item).is(':visible') ? 1 : 0;
                if (email) {
                    value.push({
                        e: email,
                        o: consent
                    });
                }
            });
            $('.js-multi-email-value', $multiEmailField).val(JSON.stringify(value));
        },
        /**
         * Adds a new item: email box and consent checkbox
         * @param $multiEmailField
         */
        addItem($multiEmailField) {
            let $newItem = $('.js-multi-email-item', $multiEmailField).first().clone(false, false);
            if ($newItem) {
                $('.js-multi-email', $newItem).attr('value', '').val('');
                $('.js-multi-email-consent', $newItem).val('');
                $('.js-multi-email-consenticon', $newItem).hide();
                $('.js-multi-email-consenticon', $newItem).first().show();
                $('.js-multi-email-items', $multiEmailField).append($newItem);
                $('.js-multi-email-remove', $multiEmailField).show();
            }
        },
        /**
         * Removes an item: email box and consent checkbox
         * @param $deleteBtn
         */
        removeItem($deleteBtn) {
            let $multiEmailField = $deleteBtn.closest('.js-multi-email');
            if (1 < $('.js-multi-email-item', $multiEmailField).length) {
                $deleteBtn.closest('.js-multi-email-item').remove();
            }
            if (1 == $('.js-multi-email-item', $multiEmailField).length) {
                $('.js-multi-email-remove', $multiEmailField).hide();
            }
        },
        /**
         * Toggle consent boxes
         * @param $consentBox
         */
        toggleConsent($consentBox) {
            let $item = $consentBox.closest('.js-multi-email-item');
            $('.js-multi-email-consenticon', $item).toggle();
        }
    },
    MultiDependField: {
        /**
         * Register function
         * @param {jQuery} container
         */
        register(container) {
            container.find('.js-multi-field').each((index, element) => {
                const inputElement = $(element);
                const fields = inputElement.find('.js-multi-field-val').data('fields');
                inputElement.find('.js-multi-field-add-item').on('click', (e) => {
                    App.Fields.MultiDependField.addRow(inputElement, fields);
                });
                App.Fields.MultiDependField.registerRow(inputElement, fields);
            });
        },
        /**
         * Register row
         * @param {jQuery} inputElement
         * @param {Object} fields
         */
        registerRow(inputElement, fields) {
            for (let i in fields) {
                inputElement.find('[name="' + fields[i] + '"]').on('change', (e) => {
                    App.Fields.MultiDependField.parseToJson(inputElement, fields);
                });
            }
            inputElement.find('.js-remove-item').on('click', (e) => {
                App.Fields.MultiDependField.removeRow($(e.target), inputElement);
                App.Fields.MultiDependField.parseToJson(inputElement.closest('.js-multi-field'), fields);
            });
        },
        /**
         * Invoked after clicking the remove button
         * @param {jQuery} element
         * @param {jQuery} container
         */
        removeRow(element, container) {
            if (container.find('.js-multi-field-row').length > 1) {
                element.closest('.js-multi-field-row').remove();
            }
        },
        /**
         * Convert data to json
         * @param {jQuery} element
         * @param {Object} fields
         */
        parseToJson(element, fields) {
            let arr = [];
            let allFields = $(element).find('.js-multi-field-row');
            let arrayLength = allFields.length;
            for (let i = 0; i < arrayLength; ++i) {
                let partData = {},
                    skip = false;
                for (let k in fields) {
                    partData[fields[k]] = $(allFields[i])
                        .find('[name="' + fields[k] + '"]')
                        .val();
                    if (k == 0 && partData[fields[k]] === '') {
                        skip = true;
                        break;
                    }
                }
                if (!skip) {
                    arr.push(partData);
                }
            }
            $(element).find('input.js-multi-field-val').val(JSON.stringify(arr));
        },
        /**
         * Invoked after clicking the add button
         * @param {jQuery} container
         * @param {Object} fields
         */
        addRow(container, fields) {
            let newField;
            let lastField = container.find('.js-multi-field-row').last();
            let selectFields = lastField.find('select.select2');
            if (selectFields.length) {
                selectFields.select2('destroy').removeAttr('data-select2-id').find('option').removeAttr('data-select2-id');
                newField = lastField.clone(false, false);
                App.Fields.Picklist.showSelect2ElementView(lastField.find('select.select2'));
            } else {
                newField = lastField.clone(false, false);
            }
            for (let i in fields) {
                newField.find('[name="' + fields[i] + '"]').val('');
            }
            newField.insertAfter(container.find('.js-multi-field-row').last());
            App.Fields.Picklist.showSelect2ElementView(newField.find('select.select2'));
            App.Fields.Date.register(newField);
            App.Fields.MultiDependField.registerRow(container, fields);
        }
    },
    DependentSelect: {
        /**
         * Get options for select from array of items (exclude children)
         * @param {Array} data {value,text,selected, children => data[]}
         * @returns {string}
         */
        generateOptionsFromData(data) {
            let html = '';
            for (let item of data) {
                let selected = false;
                if (typeof item.selected !== 'undefined' && item.selected) {
                    selected = true;
                }
                html += `<option value=${item.value}${selected ? ' selected' : ''}>${item.text}</option>`;
            }
            return html;
        },
        /**
         * Register dependent selects
         *
         * @param {jQuery} container with data- options:
         * data-slave: selector for slave element
         * data-data: array of options with children elements for slave select (see getOptions for data format)
         * data-sort: do we want to sort slave options by text when master has two items selected? if not - just append options to slave
         */
        register(container) {
            if (typeof container === 'undefined' || typeof container.length === 'undefined' || !container.length) {
                return app.errorLog('Dependend select field container is missing.');
            }
            container.each(function () {
                const masterSelect = $(this),
                    slaveSelect = $(masterSelect.data('slave')),
                    data = masterSelect.data('data');
                if (!slaveSelect.length) {
                    return app.errorLog('Could not find slave select element (data-slave attribute)');
                }
                if (!data) {
                    return app.errorLog('Could not load data (data-data attribute)');
                }
                masterSelect.on('change', (e) => {
                    let values = $(e.target).val();
                    if (!Array.isArray(values)) {
                        values = [values];
                    }
                    let children = [];
                    for (let value of values) {
                        for (let item of data) {
                            if (item.value === value) {
                                if (typeof item.children !== 'undefined') {
                                    item.children.forEach((child) => {
                                        children.push(child);
                                    });
                                }
                            }
                        }
                    }
                    if (masterSelect.data('sort')) {
                        children.sort((a, b) => {
                            return a.text.localeCompare(b.text);
                        });
                    }
                    slaveSelect.html(App.Fields.DependentSelect.generateOptionsFromData(children));
                });
                masterSelect.html(App.Fields.DependentSelect.generateOptionsFromData(data));
            });
        }
    },
    Gantt: {
        register(container, data) {
            return new GanttField(container, data);
        }
    },
    Integer: {
        /**
         * Function returns the integer in user specified format.
         * @param {number} value
         * @param {int} numberOfDecimal
         * @returns {string}
         */
        formatToDisplay(value) {
            if (!value) {
                value = 0;
            }
            let groupSeparator = CONFIG.currencyGroupingSeparator;
            let groupingPattern = CONFIG.currencyGroupingPattern;
            value = parseFloat(value).toFixed(1);
            let integer = value.toString().split('.')[0];
            if (integer.length > 3) {
                if (groupingPattern === '123,456,789') {
                    integer = integer.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + groupSeparator);
                } else if (groupingPattern === '123456,789') {
                    integer = integer.slice(0, -3) + groupSeparator + integer.slice(-3);
                } else if (groupingPattern === '12,34,56,789') {
                    integer =
                        integer.slice(0, -3).replace(/(\d)(?=(\d\d)+(?!\d))/g, '$1' + groupSeparator) +
                        groupSeparator +
                        integer.slice(-3);
                }
            }
            return integer;
        }
    },
    Double: {
        /**
         * Function returns the currency in user specified format.
         * @param {number} value
         * @param {boolean} numberOfDecimal
         * @param {int} numberOfDecimal
         * @returns {string}
         */
        formatToDisplay(value, fixed = true, numberOfDecimal = CONFIG.noOfCurrencyDecimals) {
            if (!value) {
                value = 0;
            }
            let strDecimal = value.toString().split('.')[1];
            let numberOfZerosAtTheEnd = 0;
            if (typeof strDecimal !== 'undefined') {
                for (let i = strDecimal.length - 1; i > 0; --i) {
                    if (strDecimal[i] == '0') {
                        numberOfZerosAtTheEnd++;
                    } else {
                        break;
                    }
                }
            }
            value = parseFloat(value);
            if (fixed) {
                let base = 10 ** numberOfDecimal;
                value =
                    Math.round(
                        value * base + Math.sign(value) * 0.1 ** (17 - 2 - (Math.round(value * base) / base).toString().length)
                    ) / base;
            }
            let splittedFloat = value.toString().split('.');
            let integer = splittedFloat[0];
            if (integer !== '-0' && integer !== '0') {
                integer = App.Fields.Integer.formatToDisplay(integer);
            }
            let decimal = splittedFloat[1];
            if (numberOfDecimal) {
                if (!CONFIG.truncateTrailingZeros && decimal) {
                    for (let i = 0; i < numberOfZerosAtTheEnd && decimal.length < numberOfDecimal; ++i) {
                        decimal += '0';
                    }
                }
                if (decimal) {
                    return integer + CONFIG.currencyDecimalSeparator + decimal;
                }
            }
            return integer;
        },
        /**
         * Function to get value for db format.
         * @param {string} value
         * @returns {number}
         */
        formatToDb(value) {
            if (value == undefined || value == '') {
                value = 0;
            }
            value = value.toString();
            value = value.split(CONFIG.currencyGroupingSeparator).join('');
            value = value.replace(/\s/g, '').replace(CONFIG.currencyDecimalSeparator, '.');
            return parseFloat(value);
        }
    },
    /**
     * Tree
     */
    Tree: class Tree {
        constructor(container) {
            this.container = container;
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-tree-container')) {
                return new Tree(container);
            }
            const instances = [];
            container.find('.js-tree-container').each((n, e) => {
                instances.push(new Tree($(e)));
            });
            return instances;
        }
        /**
         * Initiation
         */
        init() {
            this.modalEvent();
            this.autoCompleteEvent();
            this.clearSelectionEvent();
        }
        /**
         * Function which will handle modal view with tree
         */
        modalEvent() {
            $('.js-tree-modal', this.container)
                .off('click')
                .on('click', (_) => {
                    let sourceFieldElement = this.container.find('input.sourceField'),
                        fieldDisplayElement = this.container.find('input[name="' + sourceFieldElement.attr('name') + '_display"]');
                    AppConnector.request({
                        module: sourceFieldElement.data('module-name'),
                        view: 'TreeModal',
                        template: sourceFieldElement.data('treetemplate'),
                        fieldName: sourceFieldElement.attr('name'),
                        multiple: sourceFieldElement.data('multiple'),
                        value: sourceFieldElement.val()
                    }).done(function (requestData) {
                        app.modalEvents['treeModal'] = function (modal, instance) {
                            instance.setSelectEvent((responseData) => {
                                sourceFieldElement.val(responseData.id);
                                fieldDisplayElement.val(responseData.name).attr('readonly', true);
                                sourceFieldElement.trigger('change');
                            });
                        };
                        app.showModalWindow(requestData, { modalId: 'treeModal' });
                    });
                });
        }
        /**
         * Function which will handle the reference auto complete event registrations
         */
        autoCompleteEvent() {
            let autoCompleteElement = $('input.treeAutoComplete', this.container);
            if (autoCompleteElement.hasClass('ui-autocomplete-input')) {
                autoCompleteElement.autocomplete('destroy');
            }
            autoCompleteElement.autocomplete({
                delay: '600',
                minLength: '3',
                source: function (request, response) {
                    let inputElement = $(this.element[0]);
                    let searchValue = request.term.toLowerCase();
                    let parentElem = inputElement.closest('.js-tree-container');
                    let sourceFieldElement = $('input.sourceField', parentElem);
                    let fieldInfo = sourceFieldElement.data('fieldinfo');
                    let allValues = fieldInfo.picklistvalues;
                    let responseDataList = [];
                    for (let id in allValues) {
                        if (allValues[id].toLowerCase().indexOf(searchValue) >= 0) {
                            responseDataList.push({ label: allValues[id], value: id, id: id });
                        }
                    }
                    if (responseDataList.length <= 0) {
                        $(inputElement).val('');
                        responseDataList.push({
                            label: app.vtranslate('JS_NO_RESULTS_FOUND'),
                            type: 'no results'
                        });
                    }
                    response(responseDataList);
                },
                select: function (event, ui) {
                    let selectedItemData = ui.item;
                    if (typeof selectedItemData.type !== 'undefined' && selectedItemData.type == 'no results') {
                        return false;
                    }
                    selectedItemData.name = selectedItemData.value;
                    this.value = selectedItemData.label;
                    let element = $(this).attr('readonly', true);
                    element.closest('.js-tree-container').find('input.sourceField').val(selectedItemData.id).trigger('change');
                    return false;
                },
                change: function (event, ui) {},
                open: function (event, ui) {
                    //To Make the menu come up in the case of quick create
                    $(this).data('ui-autocomplete').menu.element.css('z-index', '100001');
                }
            });
        }
        /**
         * Function which will register reference field clear event
         */
        clearSelectionEvent() {
            $('.clearTreeSelection', this.container)
                .off('click')
                .on('click', (e) => {
                    let fieldElement = this.container.find('.sourceField');
                    $('input[name="' + fieldElement.attr('name') + '_display"]', this.container)
                        .removeAttr('readonly')
                        .val('');
                    fieldElement.val('').trigger('change');
                    e.preventDefault();
                });
        }
    },
    /**
     * TimePeriod class
     *
     * Save value as time period in 00:m format where '0' is a number of units
     * ':' is just separator
     * and 'm' is time scale/period in php date format - available formats are [m, d, H, i, s]
     * @example 10:i = 10 minutes, 2:m = 2 months, 20:H = 20 hours and so on...
     */
    TimePeriod: class TimePeriod {
        constructor(container) {
            this.container = container;
            this.value = container.val();
            if (this.value) {
                const split = this.value.split(':');
                this.time = Number(split[0]);
                this.period = split[1];
            } else {
                this.time = 0;
                this.period = 'H';
                this.value = '0:H';
                container.val(this.value);
            }
            this.injectContent();
        }

        /**
         * Register time period field/s
         *
         * @param {jQuery} container it could be input type hidden with js-time-period class
         *                           or container that contains multiple js-time-period inputs
         *
         * @example <input type="hidden" name="field_name" class="js-time-period">
         *
         * @returns {TimePeriod|TimePeriod[]} instance/s
         */
        static register(container) {
            if (container.hasClass('c-time-period')) {
                return new TimePeriod(container);
            }
            const instances = [];
            container.find('.c-time-period').each((index, value) => {
                instances.push(new TimePeriod($(value)));
            });
            return instances;
        }

        /**
         * Inject content next to container
         *
         * @returns  {jQuery}  created element with input and select
         */
        injectContent() {
            let content = `<div class="input-group c-time-period" data-js="container">
                <div class="input-group-prepend">
                    <a href class="btn btn-default c-time-period-input-modifier c-time-period-input-modifier--minus-1"><span class="fas fa-minus"></span></a>
                </div>
                <input type="number" class="form-control c-time-period-input" min="0" value="${this.time}"
                    data-validation-engine="validate[required,funcCall[Vtiger_Integer_Validator_Js.invokeValidation]]">
                <div class="input-group-append">
                    <a href class="btn btn-default c-time-period-input-modifier c-time-period-input-modifier--plus-1"><span class="fas fa-plus"></span></a>
                    <select class="select2 js-time-period-select time-period-${this.container.attr('name')}">
                        <option value="d"${this.period === 'd' ? ' selected="selected"' : ''}>${app.vtranslate('JS_DAYS_FULL')}</option>
                        <option value="H"${this.period === 'H' ? ' selected="selected"' : ''}>${app.vtranslate('JS_HOURS_FULL')}</option>
                        <option value="i"${this.period === 'i' ? ' selected="selected"' : ''}>${app.vtranslate('JS_MINUTES_FULL')}</option>
                    </select>
                </div>
            </div>`;
            this.element = this.container.parent().append(content);
            this.input = this.element.find('.c-time-period-input').eq(0);
            this.select = this.element.find('.select2').eq(0);
            this.plus1btn = this.element.find('.c-time-period-input-modifier--plus-1').eq(0);
            this.minus1btn = this.element.find('.c-time-period-input-modifier--minus-1').eq(0);
            App.Fields.Picklist.showSelect2ElementView(this.select, { width: '100px' });
            this.registerEvents();
            return this.element;
        }

        /**
         * Register events
         */
        registerEvents() {
            this.input.on('input', this.onChange.bind(this));
            this.select.on('change', this.onChange.bind(this));
            this.plus1btn.on('click', this.onPlus1Click.bind(this));
            this.minus1btn.on('click', this.onMinus1Click.bind(this));
        }

        /**
         * On change event
         *
         * @param {Event} event
         */
        onChange(event) {
            this.time = this.input.val();
            this.period = this.select.val();
            this.value = this.input.val() + ':' + this.select.val();
            this.container.val(this.value);
        }

        /**
         * Plus 1 button click event handler
         *
         * @param {Event} event
         */
        onPlus1Click(event) {
            event.preventDefault();
            event.stopPropagation();
            this.input.val(Number(this.input.val()) + 1);
            this.onChange();
        }

        /**
         * Minus 1 button click event handler
         *
         * @param {Event} event
         */
        onMinus1Click(event) {
            event.preventDefault();
            event.stopPropagation();
            if (Number(this.input.val()) > 0) {
                this.input.val(Number(this.input.val()) - 1);
                this.onChange();
            }
        }
    },
    /**
     * Multi currency
     */
    MultiCurrency: class MultiCurrency {
        constructor(container) {
            this.container = container;
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-multicurrency-container')) {
                return new MultiCurrency(container);
            }
            const instances = [];
            container.find('.js-multicurrency-container').each((n, e) => {
                instances.push(new MultiCurrency($(e)));
            });
            return instances;
        }
        /**
         * Initiation
         */
        init() {
            $('.js-multicurrency-event', this.container)
                .off('click')
                .on('click', () => {
                    let modal = $('<form>').append(this.container.find('.js-currencies-container .js-currencies-modal').clone());
                    this.registerEnableCurrencyEvent(modal);
                    this.registerResetCurrencyEvent(modal);
                    this.loadData(modal);
                    this.calculateConversionRate(modal);
                    app.showModalWindow({
                        data: modal,
                        css: {},
                        cb: (data) => {
                            let form = data.parent();
                            form.validationEngine(app.validationEngineOptionsForRecord);
                            form.on('submit', (e) => {
                                e.preventDefault();
                                if (form.validationEngine('validate') && this.saveCurrencies(form)) {
                                    let id = form.closest('.js-modal-container').attr('id');
                                    app.hideModalWindow(null, id);
                                }
                            });
                        }
                    });
                });
            this.getField().on('focusout', (e) => {
                let element = $(e.currentTarget);
                element.formatNumber();
                this.setPrice(element.val());
            });
        }
        /**
         * Loading data
         * @param {jQuery} modalContainer
         */
        loadData(modalContainer) {
            let values = JSON.parse(this.getFieldToSave().val());
            let baseCurrencyId = values['currencyId'] || CONFIG.currencyId;
            if (values['currencies'] === undefined) {
                values['currencies'] = [];
                values['currencies'][baseCurrencyId] = { price: 0 };
            }
            for (let i in values['currencies']) {
                let row = modalContainer.find('[data-currency-id="' + i + '"]');
                if (row.length) {
                    row.find('.js-enable-currency').prop('checked', true);
                    row.find('.js-currency-reset,.js-base-currency,[name^="currencies["]').prop('disabled', false);
                    row.find('.js-converted-price').val(values['currencies'][i]['price']);
                    if (i == baseCurrencyId) {
                        row.find('.js-base-currency').prop('checked', true);
                    }
                }
            }
        }
        /**
         * Set value
         * @param {number} value
         */
        setPrice(value) {
            let values = JSON.parse(this.getFieldToSave().val());
            let baseCurrencyId = values['currencyId'] || CONFIG.currencyId;
            values['currencies'] = values['currencies'] || {};
            values['currencies'][baseCurrencyId] = { price: value };
            values['currencyId'] = baseCurrencyId;
            values = $.extend({}, values);
            this.getFieldToSave().val(JSON.stringify($.extend({}, values)));
        }
        /**
         * Gets field
         */
        getField() {
            return this.container.find('.js-multicurrency-field');
        }
        /**
         * Gets field to save
         */
        getFieldToSave() {
            return this.container.find('.js-multicurrency-field-to-save');
        }
        /**
         * Save
         * @param {jQuery} modalContainer
         */
        saveCurrencies(modalContainer) {
            let enabledBaseCurrency = modalContainer.find('.js-enable-currency').filter(':checked');
            if (enabledBaseCurrency.length < 1) {
                Vtiger_Helper_Js.showMessage({
                    text: app.vtranslate('JS_PLEASE_SELECT_BASE_CURRENCY_FOR_PRODUCT'),
                    type: 'error'
                });
                return false;
            }
            let selectedBaseCurrency = modalContainer.find('.js-base-currency').filter(':checked');
            if (selectedBaseCurrency.length < 1) {
                Vtiger_Helper_Js.showMessage({
                    text: app.vtranslate('JS_PLEASE_ENABLE_BASE_CURRENCY_FOR_PRODUCT'),
                    type: 'error'
                });
                return false;
            }

            let selectedRow = selectedBaseCurrency.closest('tr');
            let symbol = selectedRow.data('currency-symbol');
            this.container.find('.js-currency').text(symbol);
            let data = {};
            data['currencies'] = {};
            enabledBaseCurrency.closest('tr').each((n, e) => {
                let row = $(e),
                    currencyId = row.data('currencyId');
                data['currencies'][currencyId] = {};
                data['currencies'][currencyId]['price'] = row.find('.js-converted-price').val();
                if (row.find('.js-base-currency:checked').length) {
                    data['currencyId'] = currencyId;
                }
            });
            this.getFieldToSave().val(JSON.stringify(data));
            this.getField().val(selectedRow.find('.js-converted-price').val());
            selectedBaseCurrency.prop('checked', false);
            return true;
        }
        /**
         * Calculate
         * @param {jQuery} container
         */
        calculateConversionRate(container) {
            let baseCurrencyConversionRate = container
                .find('.js-base-currency')
                .filter(':checked')
                .closest('tr')
                .find('.js-conversion-rate');
            if (baseCurrencyConversionRate.val() == '1') {
                return;
            }
            let baseCurrencyRatePrevValue = baseCurrencyConversionRate.getNumberFromValue();
            container.find('.js-conversion-rate').each(function (key, domElement) {
                let element = $(domElement);
                if (!element.is(baseCurrencyConversionRate)) {
                    element.val(
                        App.Fields.Double.formatToDisplay(element.getNumberFromValue() / baseCurrencyRatePrevValue, false)
                    );
                }
            });
            baseCurrencyConversionRate.val('1');
        }
        /**
         * Function to register event for enabling currency on checkbox checked
         * @param {jQuery} container
         */
        registerEnableCurrencyEvent(container) {
            container.on('change', '.js-enable-currency', (e) => {
                let element = $(e.currentTarget);
                let parentRow = element.closest('tr');
                if (element.is(':checked')) {
                    element.attr('checked', 'checked');
                    let price = this.getField().getNumberFromValue() * parentRow.find('.js-conversion-rate').getNumberFromValue();
                    $('input', parentRow).removeAttr('disabled');
                    parentRow.find('.js-currency-reset').removeAttr('disabled');
                    parentRow.find('.js-converted-price').val(App.Fields.Double.formatToDisplay(price));
                } else {
                    if (parentRow.find('.js-base-currency').is(':checked')) {
                        app.showNotify({
                            type: 'error',
                            title:
                                '"' +
                                parentRow.find('.js-currency-name').text() +
                                '" ' +
                                app.vtranslate('JS_BASE_CURRENCY_CHANGED_TO_DISABLE_CURRENCY')
                        });
                        element.prop('checked', true);
                        return;
                    }
                    parentRow.find('input').attr('disabled', 'disabled');
                    parentRow.find('.js-currency-reset').attr('disabled', 'disabled');
                    element.removeAttr('disabled checked');
                }
            });
        }

        /**
         * Function to register event for reseting the currencies
         * @param {jQuery} container
         */
        registerResetCurrencyEvent(container) {
            container.on('click', '.js-currency-reset', (e) => {
                let parentElem = $(e.currentTarget).closest('tr');
                let price = this.getField().getNumberFromValue() * parentElem.find('.js-conversion-rate').getNumberFromValue();
                $('.js-converted-price', parentElem).val(App.Fields.Double.formatToDisplay(price));
            });
        }
    },
    /**
     * Meeting URL
     */
    MeetingUrl: class MeetingUrl {
        constructor(container) {
            this.container = container;
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-meeting-container')) {
                return new MeetingUrl(container);
            }
            const instances = [];
            container.find('.js-meeting-container').each((n, e) => {
                instances.push(new MeetingUrl($(e)));
            });
            return instances;
        }
        /**
         * Initiation
         */
        init() {
            let addButton = $('.js-meeting-add', this.container);
            if (!addButton.length) {
                return false;
            }

            let valElement = $('.js-meeting-val', this.container);
            addButton.off('click').on('click', (e) => {
                let progressIndicatorElement = $.progressIndicator({ blockInfo: { enabled: true } });
                AppConnector.request(this.getUrl(e))
                    .done((data) => {
                        let result = data.result;
                        if (result && result.success && result.url) {
                            valElement.attr('readonly', true).val(result.url);
                        } else {
                            app.showNotify({
                                text: app.vtranslate('JS_ERROR'),
                                type: 'error'
                            });
                        }
                        progressIndicatorElement.progressIndicator({ mode: 'hide' });
                    })
                    .fail((_) => {
                        app.showNotify({
                            text: app.vtranslate('JS_ERROR'),
                            type: 'error'
                        });
                        progressIndicatorElement.progressIndicator({ mode: 'hide' });
                    });
            });
            $('.js-meeting-clear', this.container)
                .off('click')
                .on('click', () => {
                    valElement.attr('readonly', false).val('');
                });
            this.addEventsForDependentFields();
        }
        /**
         * Gets URL
         */
        getUrl(e) {
            let url = e.currentTarget.dataset.url;
            let formData = $(e.currentTarget).closest('form').serializeFormData();
            let expField = e.currentTarget.dataset.expField;
            if (expField && formData && formData[expField]) {
                let date = formData[expField].split(' ');
                url += '&exp=' + encodeURIComponent(date[0]);
            }
            let roomName = e.currentTarget.dataset.roomName;
            if (roomName && formData && formData[roomName]) {
                url += '&roomName=' + encodeURIComponent(formData[roomName]);
            }
            return url;
        }
        /**
         * Add events for dependent fields
         */
        addEventsForDependentFields() {
            let addButton = $('.js-meeting-add', this.container);
            let valElement = $('.js-meeting-val', this.container);
            let data = addButton.data();
            let formElement = this.container.closest('form');
            for (let name of ['expField', 'roomName']) {
                let fieldName = data[name];
                if (!fieldName) {
                    continue;
                }
                formElement.on('change', `[name=${fieldName}]`, (_) => {
                    if (data['domain'] && valElement.val().indexOf(data['domain']) === 0) {
                        addButton.trigger('click');
                        app.showNotify({
                            type: 'info',
                            text: app.vtranslate('JS_MEETING_URL_CHANGED')
                        });
                    }
                });
            }
        }
    },
    /**
     * Changes Json
     */
    ChangesJson: class ChangesJson {
        constructor(container) {
            this.container = container;
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-changesjson-container')) {
                return new ChangesJson(container);
            }
            const instances = [];
            container.find('.js-changesjson-container').each((_, e) => {
                instances.push(new ChangesJson($(e)));
            });
            return instances;
        }
        /**
         * Initiation
         */
        init() {
            $('.js-changesjson-edit', this.container)
                .off('click')
                .on('click', () => {
                    let field = this.getField();
                    let value = field.val() ? JSON.parse(field.val()) : { record: 0, module: '', changes: [] };
                    let relatedField = this.getRelatedField();
                    if (relatedField.length) {
                        value.record = relatedField.val();
                        value.module = $('input[name="popupReferenceModule"]', relatedField.closest('.fieldValue')).val();
                    }
                    if (!value.record || value.record == 0) {
                        app.showNotify({ text: app.vtranslate('JS_LACK_INFORMATION_ABOUT_RECORD') });
                        return false;
                    }
                    let progressIndicatorElement = $.progressIndicator({ blockInfo: { enabled: true } });
                    AppConnector.request({
                        module: value.module,
                        record: value.record,
                        changes: value.changes,
                        sourceModule: field.data('module'),
                        sourceField: field.attr('name'),
                        view: 'ChangesJsonModal'
                    })
                        .done((requestData) => {
                            progressIndicatorElement.progressIndicator({ mode: 'hide' });
                            app.showModalWindow({
                                data: requestData,
                                css: {},
                                cb: (data) => {
                                    this.saveData(data, value);
                                }
                            });
                        })
                        .fail((_) => {
                            app.showNotify({
                                text: app.vtranslate('JS_ERROR'),
                                type: 'error'
                            });
                            progressIndicatorElement.progressIndicator({ mode: 'hide' });
                        });
                });
        }
        /**
         * Save data to field
         */
        saveData(container, data) {
            let form = container.find('form');
            container.on('click', '.js-modal__save', (e) => {
                if (form.validationEngine('validate')) {
                    e.preventDefault();
                    if (!form.find('input[id^="selectRow"]:checked').length) {
                        app.showNotify({
                            text: app.vtranslate('JS_NONE_FIELD_MARKED_IN_MASS_EDIT'),
                            type: 'error'
                        });
                        return;
                    }
                    let invalidFields = form.data('jqv').InvalidFields;
                    if (invalidFields.length !== 0) {
                        return;
                    }
                    form.find('[id^="selectRow"]').each(function (_, checkbox) {
                        checkbox = $(checkbox);
                        if (!checkbox.prop('checked')) {
                            checkbox
                                .closest('.js-form-row-container')
                                .find('.fieldValue [name]')
                                .each(function (_, element) {
                                    element = $(element);
                                    element.attr('data-element-name', element.attr('name')).removeAttr('name');
                                });
                        }
                    });
                    let changeData = form.serializeFormData();
                    delete changeData['_csrf'];
                    for (let fieldName in changeData) {
                        if (fieldName.substr(-2) === '[]') {
                            let fieldNameShort = fieldName.substr(0, fieldName.length - 2);
                            changeData[fieldNameShort] = changeData[fieldName];
                            delete changeData[fieldName];
                        }
                    }
                    data.changes = changeData;
                    this.getField().val(JSON.stringify(data));
                    app.hideModalWindow(null, form.closest('.js-modal-container').attr('id'));
                }
            });
        }
        /**
         * Gets field
         */
        getField() {
            return this.container.find('.js-changesjson-value');
        }
        /**
         * Gets related field
         */
        getRelatedField() {
            let relatedFieldName = this.getField().data('related-field');
            return this.container.closest('form').find(`[name=${relatedFieldName}]`);
        }
    },
    /**
     * MultiReference
     */
    MultiReference: class MultiReference {
        constructor(container) {
            this.container = container;
            this.select = container.find('.js-multi-reference');
            this.form = container.closest('form');
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-multiReference-container')) {
                return new MultiReference(container);
            }
            const instances = [];
            container.find('.js-multiReference-container').each((_, e) => {
                instances.push(new MultiReference($(e)));
            });
            return instances;
        }
        /**
         * Set a value for the field
         *
         * @param   {jQuery}  field Field element
         * @param   {mixed}  value The value to set
         * @param   {object}  params Additional parameters [optional]
         *
         * @return  {mixed} The value that has been set
         */
        static setValue(field, value, params) {
            if (!(params && params['extend'])) {
                field.val(null);
            }
            const values = field.val();
            $.each(value, (id, label) => {
                if (!values.includes(id)) {
                    field.append(new Option(label, id, true, true));
                }
            });
            field.trigger('change');
            return field.val();
        }
        /**
         * Initiation
         */
        init() {
            $('.js-related-popup', this.container)
                .off('click')
                .on('click', () => {
                    app.showRecordsList(this.getParams(), (_modal, instance) => {
                        instance.setSelectEvent((data) => {
                            this.setReferenceFieldValue(data);
                        });
                    });
                });
            $('.js-create-reference-record', this.container)
                .off('click')
                .on('click', () => {
                    this.createHandler();
                });
            this.registerAutoComplete();
        }
        /**
         * Function which will handle the reference auto complete event registrations
         */
        registerAutoComplete() {
            App.Fields.Picklist.showSelect2ElementView(this.select, {
                ajax: {
                    data: function (item) {
                        return {
                            search_value: item.term ?? '',
                            page: item.page
                        };
                    },
                    processResults: (data, params) => {
                        let items = new Array();
                        if (!params.term) {
                            items.push({
                                type: 'optgroup',
                                name: this.select.attr('placeholder')
                            });
                        } else if (data.success) {
                            $.each(data.result, (_, item) => {
                                items.push({
                                    name: item.label,
                                    id: item.id
                                });
                            });
                        }
                        return {
                            results: items,
                            pagination: {
                                more: false
                            }
                        };
                    }
                }
            });
        }
        /**
         * Set reference field value
         */
        createHandler() {
            let formData = this.form.serializeFormData();
            delete formData['action'];
            App.Components.QuickCreate.createRecord($('.js-popup-reference-module', this.container).val(), {
                data: {
                    sourceRecordData: formData
                },
                callbackFunction: (data) => {
                    if (data.success) {
                        this.select.append(new Option(data.result._recordLabel, data.result._recordId, true, true));
                    }
                },
                noCache: true
            });
        }
        /**
         * Set reference field value
         * @param {object} data
         */
        setReferenceFieldValue(data) {
            const values = this.select.val();
            $.each(data, (id, label) => {
                if (!values.includes(id)) {
                    this.select.append(new Option(label, id, true, true));
                }
            });
        }
        /**
         * Gets params
         * @returns {Object}
         */
        getParams() {
            const referenceModule = $('.js-popup-reference-module', this.container).val(),
                sourceFieldName = this.select.attr('name').slice(0, -2),
                sourceRecordElement = $('input[name="record"]', this.form),
                listFilterFieldsJson = this.form.find('input[name="listFilterFields"]').val(),
                listFilterFields = listFilterFieldsJson ? JSON.parse(listFilterFieldsJson) : [];
            let sourceRecordId = '';
            if (sourceRecordElement.length > 0) {
                sourceRecordId = sourceRecordElement.val();
            }
            let filterFields = {};
            if (
                listFilterFields[sourceFieldName] != undefined &&
                listFilterFields[sourceFieldName][referenceModule] != undefined
            ) {
                $.each(listFilterFields[sourceFieldName][referenceModule], (index) => {
                    let mapFieldElement = this.form.find('[name="' + index + '"]');
                    if (mapFieldElement.length && mapFieldElement.val() != '') {
                        filterFields[index] = mapFieldElement.val();
                    }
                });
            }
            return {
                module: referenceModule,
                src_module: $('input[name="module"]', this.form).val(),
                src_field: sourceFieldName,
                src_record: sourceRecordId,
                filterFields: filterFields,
                multi_select: true
            };
        }
    },
    /**
     * Password
     */
    Password: class Password {
        constructor(container) {
            this.container = container;
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-pwd-container')) {
                return new Password(container);
            }
            const instances = [];
            container.find('.js-pwd-container').each((_, e) => {
                instances.push(new Password($(e)));
            });
            return instances;
        }
        /**
         * Get strength meter
         * @returns {Object}
         */
        static getStrengthLevels() {
            if (!this.strengthLevels) {
                this.strengthLevels = {
                    0: app.vtranslate('JS_PWD_RIDICULOUS'),
                    1: app.vtranslate('JS_PWD_VERY_WEAK'),
                    2: app.vtranslate('JS_PWD_WEAK'),
                    3: app.vtranslate('JS_PWD_MEDIUM'),
                    4: app.vtranslate('JS_PWD_STRONG'),
                    5: app.vtranslate('JS_PWD_VERY_STRONG')
                };
            }
            return { ...this.strengthLevels };
        }
        /**
         * Initiation
         */
        init() {
            const field = this.getField();
            $('.js-pwd-auto-generate', this.container)
                .off('click')
                .on('click', (e) => {
                    this.getResponse($(e.currentTarget).data('url')).then((response) => {
                        if (response.success && response.result && response.result.pwd) {
                            this.clear();
                            field.val(response.result.pwd).trigger('keyup').focus();
                        }
                    });
                });
            $('.js-pwd-validate', this.container)
                .off('click')
                .on('click', (e) => {
                    this.getResponse($(e.currentTarget).data('url') + '&password=' + field.val()).then((response) => {
                        if (response.success && response.result) {
                            let message = response.result.message;
                            if (Array.isArray(message)) {
                                message = message.join('<br>');
                            }
                            field.validationEngine('showPrompt', message, response.result.type, 'topLeft', true);
                            field.validationEngine('updatePromptsPosition');
                        }
                    });
                });
            $('.js-pwd-clear', this.container)
                .off('click')
                .on('click', () => {
                    this.clear();
                });
            $('.js-pwd-copy', this.container)
                .off('click')
                .on('click', () => {
                    if (this.container.find('.js-pwd-show').attr('disabled') === 'disabled') {
                        this.getPassword().then((response) => {
                            this.clear();
                            field.val(response.result.text);
                            ClipboardJS.copy(response.result.text);
                            app.showNotify({
                                text: app.vtranslate('JS_NOTIFY_COPY_TEXT'),
                                type: 'success'
                            });
                        });
                    } else {
                        ClipboardJS.copy(this.getField().val());
                        app.showNotify({
                            text: app.vtranslate('JS_NOTIFY_COPY_TEXT'),
                            type: 'success'
                        });
                    }
                });
            $('.js-pwd-get', this.container)
                .off('click')
                .on('click', () => {
                    this.getPassword().then((response) => {
                        this.clear();
                        field.val(response.result.text);
                    });
                });
            if (field.data('strengthMeter')) {
                field.off('keyup').on('keyup', (e) => {
                    let score = this.strengthMeter(e.target.value || '');
                    field
                        .attr('data-original-title', App.Fields.Password.getStrengthLevels()[score])
                        .tooltip('show')
                        .validationEngine('hide');
                });
            }
        }
        /**
         * Get decoded password
         * @returns {Promise}
         */
        getPassword() {
            const field = this.getField();
            return this.getResponse({
                module: field.data('module'),
                field: field.attr('name'),
                record: $('input[name="record"]', this.container.closest('form')).val() || app.getRecordId(),
                action: 'Password',
                mode: 'getPwd'
            });
        }
        /**
         * Clear data
         */
        clear() {
            this.getField().val('').attr('disabled', false).tooltip('dispose').validationEngine('hide');
            this.container.find('.js-pwd-validate, .js-pwd-show').attr('disabled', false);
        }
        /**
         * Get response
         * @param {Object|string} params
         * @returns {Promise}
         */
        getResponse(params) {
            const aDeferred = $.Deferred();
            let progressIndicatorElement = $.progressIndicator({ blockInfo: { enabled: true } });
            AppConnector.request(params)
                .done((response) => {
                    progressIndicatorElement.progressIndicator({ mode: 'hide' });
                    if (response.success) {
                        aDeferred.resolve(response);
                    } else {
                        aDeferred.reject(response);
                    }
                })
                .fail((_) => {
                    app.showNotify({
                        text: app.vtranslate('JS_ERROR'),
                        type: 'error'
                    });
                    progressIndicatorElement.progressIndicator({ mode: 'hide' });
                    aDeferred.reject(_);
                });
            return aDeferred.promise();
        }
        /**
         * Get strength meter score
         * @param {string} pwd
         * @returns {int}
         */
        strengthMeter(pwd) {
            let score = 0;
            if (pwd.length > 6) score++;
            if (pwd.match(/[a-z]/) && pwd.match(/[A-Z]/)) score++;
            if (pwd.match(/\d+/)) score++;
            if (pwd.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/)) score++;
            if (pwd.length > 12) score++;

            return score;
        }
        /**
         * Gets field
         */
        getField() {
            return this.container.find('.js-pwd-field');
        }
    },
    /**
     * Multi Attachment
     */
    MultiAttachment: class MultiAttachment {
        /**
         * Constructor
         * @param {jQuery} container
         * @param {Object} options
         */
        constructor(container, options) {
            this.container = container;
            this.fileInput = container.find('.js-multi-attachment__file').eq(0);
            this.dataInput = container.find('.js-multi-attachment__values');
            this.form = container.closest('form');

            this.progressBar = container.find('.js-multi-attachment__progress-bar');
            this.progress = container.find('.js-multi-attachment__progress');
            this.result = container.find('.js-multi-attachment__result');
            this.files = this.dataInput.is('input') ? JSON.parse(this.dataInput.val()) : this.dataInput.data('value');

            let fieldInfo = this.dataInput.data('fieldinfo') || {};
            this.options = {
                formats: fieldInfo.formats || [],
                limit: fieldInfo.limit || 1,
                maxFileSize: fieldInfo.maxFileSize,
                maxFileSizeDisplay: fieldInfo.maxFileSizeDisplay || '',
                ...options
            };
            if (this.form.length && this.fileInput.length) {
                this.initEditView();
            } else {
                this.initDetailView();
            }
        }
        /**
         * Register function
         * @param {jQuery} container
         * @param {Object} options
         */
        static register(container, options = {}) {
            if (container.hasClass('js-multi-attachment')) {
                return new MultiAttachment(container, options);
            }
            const instances = [];
            container.find('.js-multi-attachment').each((_, e) => {
                instances.push(new MultiAttachment($(e), options));
            });
            return instances;
        }
        /**
         * Initiation for detail view
         */
        initDetailView() {
            this.files.forEach((fileInfo) => {
                this.createItem(fileInfo);
            });
        }
        /**
         * Initiation for edit view
         */
        initEditView() {
            this.fileInput.detach();
            this.container.on('mouseup', this.openBrowser.bind(this));
            this.fileInput.fileupload({
                dataType: 'json',
                replaceFileInput: false,
                fileInput: this.fileInput,
                autoUpload: false,
                submit: this.submit.bind(this),
                add: this.add.bind(this),
                progressall: this.progressAll.bind(this),
                change: this.change.bind(this),
                drop: this.change.bind(this),
                dragover: this.dragOver.bind(this),
                fail: this.uploadError.bind(this),
                done: this.uploadSuccess.bind(this)
            });
            this.container.on('dragleave', this.dragLeave.bind(this));
            this.container.on('dragend', this.dragLeave.bind(this));
            this.fileInput.fileupload('option', 'dropZone', this.container);
            this.enableDragNDrop();
            this.form.on('submit', this.onFormSubmit.bind(this));
            this.deleteButtonActive = true;
            this.container.on('click', '.js-multi-attachment__file-buttons-delete', (e) => {
                e.preventDefault();
                this.deleteFile(e.currentTarget.dataset.key);
            });
            this.files.forEach((fileInfo) => {
                this.createItem(fileInfo);
            });
            this.filesActive = 0;
        }
        /**
         * Add event handler from jQuery-file-upload
         *
         * @param {Event} e
         * @param {Object} data
         */
        add(e, data) {
            if (data.files.length > 0) {
                data.submit();
            }
        }
        /**
         * Submit event handler from jQuery-file-upload
         *
         * @param {Event} e
         * @param {Object} data
         */
        submit(e, data) {
            this.filesActive++;
            this.progressInstance = $.progressIndicator({
                position: 'replace',
                blockInfo: {
                    enabled: true,
                    elementToBlock: this.container
                }
            });
        }
        /**
         * Prevent form submission before file upload end
         * @param e
         */
        onFormSubmit(e) {
            if (this.filesActive) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                app.showAlert(app.vtranslate('JS_WAIT_FOR_FILE_UPLOAD'));
                return false;
            }
            return true;
        }
        /**
         * Progressall event handler from jQuery-file-upload
         *
         * @param {Event} e
         * @param {Object} data
         */
        progressAll(e, data) {
            const progress = parseInt((data.loaded / data.total) * 100, 10);
            this.progressBar.css({ width: progress + '%' });
            if (progress === 100) {
                setTimeout(() => {
                    this.progress.addClass('d-none');
                    this.progressBar.css({ width: '0%' });
                }, 1000);
            } else {
                this.progress.removeClass('d-none');
            }
        }
        /**
         * File change event handler from jQuery-file-upload
         *
         * @param {Event} e
         * @param {object} data
         */
        change(e, data) {
            let { valid, error } = this.filterFiles(data.files);
            data.files = valid;
            if (!valid.length) {
                this.fileInput.val('');
            }
            if (error.length) {
                this.showErrors(error);
            }
            this.dragLeave(e);
        }
        /**
         * Get only valid files from list
         *
         * @param {Array} files
         * @returns {Object}
         */
        filterFiles(files) {
            let valid = [],
                error = [];
            if (files.length + this.files.length > this.options.limit) {
                error.push({ error: { text: `${app.vtranslate('JS_FILE_LIMIT')} [${this.options.limit}]` } });
            } else {
                for (let file of files) {
                    this.validateFileType(file) && this.validateFileSize(file) ? valid.push(file) : error.push(file);
                }
            }
            return { valid, error };
        }
        /**
         * Validate maximum file size
         * @param {Object} file
         * @returns {Boolean}
         */
        validateFileSize(file) {
            let result = typeof file.size === 'number' && file.size < this.options.maxFileSize;
            if (!result) {
                file.error = {
                    title: `${app.vtranslate('JS_UPLOADED_FILE_SIZE_EXCEEDS')} <br> [${this.options.maxFileSizeDisplay}]`,
                    text: file.name
                };
            }
            return result;
        }
        /**
         * Validate file
         *
         * @param {Object} file
         * @returns {boolean}
         */
        validateFileType(file) {
            let result =
                !this.options.formats.length ||
                this.options.formats.filter((format) => {
                    return file.type === format || (format.slice(-2) === '/*' && file.type.indexOf(format.slice(0, -1)) === 0);
                }).length > 0;
            if (!result) {
                file.error = { title: app.vtranslate('JS_INVALID_FILE_TYPE'), text: file.name };
            }
            return result;
        }
        /**
         * Show errors
         */
        showErrors(errors = []) {
            for (let info of errors) {
                this.showError(info.error);
            }
        }
        /**
         * Show error
         */
        showError(error) {
            if (typeof error.type === 'undefined') {
                error.type = 'error';
            }
            error.textTrusted = false;
            app.showNotify(error);
        }
        /**
         * Dragover event handler from jQuery-file-upload
         *
         * @param {Event} e
         */
        dragOver(_e) {
            this.container.addClass('c-multi-image__drop-effect');
        }
        /**
         * Dragleave event handler
         * @param {Event} e
         */
        dragLeave(_e) {
            this.container.removeClass('c-multi-image__drop-effect');
        }
        /**
         * Error event handler from file upload request
         *
         * @param {Event} e
         * @param {Object} data
         */
        uploadError(_e, data) {
            this.progressInstance.progressIndicator({ mode: 'hide' });
            this.filesActive--;
            app.errorLog('File upload error.');
            const { jqXHR, files } = data;
            if (typeof jqXHR.responseJSON === 'undefined' || jqXHR.responseJSON === null) {
                return this.showError({
                    title: app.vtranslate('JS_FILE_UPLOAD_ERROR'),
                    type: 'error'
                });
            }
            files.forEach((file) => {
                this.showError({
                    title: app.vtranslate('JS_FILE_UPLOAD_ERROR'),
                    text: file.name,
                    type: 'error'
                });
            });
            this.updateFormValues();
        }
        /**
         * Success event handler from file upload request
         *
         * @param {Event} e
         * @param {Object} data
         */
        uploadSuccess(e, data) {
            this.progressInstance.progressIndicator({ mode: 'hide' });
            const { result } = data;
            const attach = result.result.attach;
            attach.forEach((fileAttach) => {
                this.filesActive--;
                if (typeof fileAttach.key === 'undefined') {
                    return this.uploadError(e, data);
                }
                if (typeof fileAttach.info !== 'undefined' && fileAttach.info) {
                    app.showNotify({
                        type: 'notice',
                        text: fileAttach.info
                    });
                }
                this.files.push(fileAttach);
                const fileInfo = this.getFileInfo(fileAttach.key);
                this.createItem(fileInfo);
            });
            this.updateFormValues();
        }
        /**
         * Get file information
         *
         * @param {String} key - file id
         * @returns {Object}
         */
        getFileInfo(key) {
            for (let i = 0, len = this.files.length; i < len; i++) {
                const file = this.files[i];
                if (file.key === key) {
                    return file;
                }
            }
            app.errorLog(`File '${key}' not found.`);
            app.showNotify({
                text: app.vtranslate('JS_INVALID_FILE_HASH'),
                type: 'error'
            });
        }
        /**
         * Generate preview of image as html string from existing values
         * @param {Object} file
         */
        createItem(file) {
            const item = document.createElement('fieldset');
            item.setAttribute('class', 'c-multi-attachment--file bg-light js-handle');
            item.setAttribute('data-key', file.key);

            const legend = document.createElement('legend');
            legend.appendChild(document.createTextNode(file.name));
            item.appendChild(legend);

            const icon = document.createElement('div');
            icon.setAttribute('class', 'c-multi-attachment--file-icon');
            const span = document.createElement('span');
            span.setAttribute('class', file.icon);
            icon.appendChild(span);
            item.appendChild(icon);

            const fileInfo = document.createElement('div');
            fileInfo.setAttribute('class', 'c-multi-attachment--file-info');
            const name = document.createElement('span');
            name.setAttribute('class', 'c-multi-attachment--file-info-main');
            name.setAttribute('aria-hidden', true);
            name.appendChild(document.createTextNode(file.name));
            fileInfo.appendChild(name);
            const size = document.createElement('span');
            size.setAttribute('class', 'c-multi-attachment--file-info-sub');
            size.appendChild(document.createTextNode(file.sizeDisplay));
            fileInfo.appendChild(size);
            item.appendChild(fileInfo);

            const buttons = document.createElement('div');
            buttons.setAttribute('class', 'js-multi-attachment__file-buttons');

            if (file.url) {
                const downloadBtn = document.createElement('a');
                downloadBtn.setAttribute('class', 'btn btn-sm btn-outline-success js-multi-attachment__file-buttons-download');
                downloadBtn.setAttribute('href', file.url);
                downloadBtn.setAttribute('download', file.name);
                downloadBtn.setAttribute('title', $('<textarea />').html(app.vtranslate('JS_DOWNLOAD')).text());
                const downloadBtnIcon = document.createElement('span');
                downloadBtnIcon.setAttribute('class', 'fa fa-download');
                downloadBtn.appendChild(downloadBtnIcon);
                buttons.appendChild(downloadBtn);
            }
            if (this.deleteButtonActive && !file.lock) {
                const deleteBtn = document.createElement('button');
                deleteBtn.setAttribute('class', 'btn btn-sm btn-outline-danger js-multi-attachment__file-buttons-delete ml-1');
                deleteBtn.setAttribute('data-key', file.key);
                deleteBtn.setAttribute('title', $('<textarea />').html(app.vtranslate('JS_DELETE')).text());
                const deleteBtnIcon = document.createElement('span');
                deleteBtnIcon.setAttribute('class', 'fa fa-trash-alt');
                deleteBtn.appendChild(deleteBtnIcon);
                buttons.appendChild(deleteBtn);
            }

            item.appendChild(buttons);
            this.result.append(item);
        }
        /**
         * Enable drag and drop files repositioning
         */
        enableDragNDrop() {
            this.result
                .sortable({
                    containment: this.container,
                    items: '.js-handle',
                    stop: this.sortStop.bind(this)
                })
                .disableSelection();
        }
        /**
         * Prevent form submission
         *
         * @param {Event} e
         */
        openBrowser(e) {
            if (!e.target.closest('fieldset')) {
                e.preventDefault();
                this.fileInput.trigger('click');
            }
        }

        /**
         * Update file position according to elements order
         *
         * @param {Event} e
         * @param {Object} ui
         */
        sortStop(e, ui) {
            const actualElements = this.result.find('fieldset').toArray();
            this.files = actualElements.map((element) => {
                for (let i = 0, len = this.files.length; i < len; i++) {
                    const elementHash = $(element).data('key');
                    if (this.files[i].key === elementHash) {
                        return this.files[i];
                    }
                }
            });
            this.updateFormValues();
        }
        /**
         * Remove file from preview and from file list
         *
         * @param {String} key
         */
        deleteFile(key) {
            const fileInfo = this.getFileInfo(key);
            this.result.find(`[data-key="${fileInfo.key}"]`).remove();
            this.files = this.files.filter((file) => file.key !== fileInfo.key);
            this.updateFormValues();
        }

        /**
         * Update form input values
         */
        updateFormValues() {
            this.fileInput.val('');
            const formValues = this.files.map((file) => {
                return { key: file.key, name: file.name, size: file.size, type: file.type };
            });
            this.dataInput.val(JSON.stringify(formValues));
        }
    },
    /**
     * Icon
     */
    Icon: class Icon {
        constructor(container) {
            this.container = container;
            this.init();
        }
        /**
         * Register function
         * @param {jQuery} container
         */
        static register(container) {
            if (container.hasClass('js-icon-container')) {
                return new Icon(container);
            }
            const instances = [];
            container.find('.js-icon-container').each((_, e) => {
                instances.push(new Icon($(e)));
            });

            return instances;
        }
        /**
         * Initiation
         */
        init() {
            this.iconElement = $('.js-icon-show', this.container);
            $('.js-clear-selection', this.container)
                .off('click')
                .on('click', () => {
                    this.clear();
                });
            $('.js-icon-select', this.container)
                .off('click')
                .on('click', () => {
                    App.Components.Icons.modalView().done((data) => {
                        if (data.type === 'icon') {
                            const span = document.createElement('span');
                            span.setAttribute('class', data.name);
                            this.iconElement.html('').append(span);
                        } else if (data.type === 'image') {
                            const image = document.createElement('img');
                            image.setAttribute('class', 'icon-img--picklist');
                            image.setAttribute('src', data.src);
                            this.iconElement.html('').append(image);
                        }
                        this.setValue(data);
                        this.setDisplayValue(data.name);
                    });
                });
        }
        /**
         * Clear selection
         */
        clear() {
            let element = this.getField();
            let fieldName = element.attr('name');
            element.val('');
            this.container.find(`#${fieldName}_display`).val('');
            this.iconElement.html('');
        }
        /**
         * Set icon name
         * @param {string} data
         */
        setDisplayValue(name) {
            let fieldName = this.getField().attr('name');
            this.container.find(`#${fieldName}_display`).val(name).attr('readonly', true);
        }
        /**
         * Set value
         * @param {Object} data
         */
        setValue(data) {
            let { type, name } = data;
            if (data.key) {
                name = data.key;
            }
            this.getField().val(JSON.stringify({ type: type, name: name }));
        }
        /**
         * Gets field
         */
        getField() {
            return this.container.find('.js-source-field');
        }
    },
    Utils: {
        registerMobileDateRangePicker(element) {
            this.hideMobileKeyboard(element);
            if (!Quasar.plugins.Platform.is.desktop) {
                element
                    .on('showCalendar.daterangepicker', (ev, picker) => {
                        picker.container.addClass('js-visible');
                    })
                    .on('hide.daterangepicker', (ev, picker) => {
                        picker.container.removeClass('js-visible');
                    });
            }
        },
        hideMobileKeyboard(element) {
            if (!Quasar.plugins.Platform.is.desktop) {
                element.attr('readonly', 'true').addClass('bg-white');
            }
        },
        positionPicker(ev, picker) {
            let offset = picker.element.offset();
            let $window = $(window);
            if (offset.left - $window.scrollLeft() + picker.container.outerWidth() > $window.width()) {
                picker.opens = 'left';
            } else {
                picker.opens = 'right';
            }
            picker.move();
            if (offset.top - $window.scrollTop() + picker.container.outerHeight() > $window.height()) {
                picker.drops = 'up';
            } else {
                picker.drops = 'down';
            }
            picker.move();
        },
        /**
         * Set a value for the field
         *
         * @param   {jQuery}  field Field element
         * @param   {mixed}  value The value to set
         * @param   {object}  params Additional parameters [optional]
         * @param   {boolean}  animation
         */
        setValue(field, value, params, animation = true) {
            const fieldInfo = field.data('fieldinfo');
            switch (fieldInfo['type']) {
                case 'picklist':
                case 'languages':
                case 'country':
                case 'currencyList':
                case 'modules':
                    App.Fields.Picklist.setValue(field, value, params);
                    break;
                case 'multiReference':
                    App.Fields.MultiReference.setValue(field, value, params);
                    break;
                default:
                    field.val(value);
                    break;
            }
            if (animation) {
                const fieldValue = field.closest('.fieldValue');
                fieldValue.addClass('border border-info');
                setTimeout(function () {
                    fieldValue.removeClass('border border-info');
                }, 5000);
            }
        }
    }
};