templates/js/app.js.twig line 1

Open in your IDE?
  1. let domDispatcher = new DomDispatcher();
  2. domDispatcher.init();
  3. initBearer.set(INIT_BEARERS.GLOBAL_DOM_DISPATCHER, domDispatcher);
  4. $('[data-remove-image-button]').click(function () {
  5.     if (confirm("Удалить файл?")) {
  6.         let removeBtn = $(this)[0];
  7.         let image = Dom.findParentChild(removeBtn, "[data-remove-image]");
  8.         let flag = Dom.findParentChild(removeBtn, "[data-remove-image-flag]");
  9.         let fileInputContainer = Dom.findParentChild(removeBtn, ".new-file-input");
  10.         let fileInput = $(fileInputContainer).find('input');
  11.         image.remove();
  12.         flag.value = "true";
  13.         let addNewFileBtn = Dom.findParentChild($(this)[0], ".upload-new-file-btn");
  14.         if (addNewFileBtn) {
  15.             $(addNewFileBtn).attr("hidden", true);
  16.         }
  17.         $(fileInputContainer).attr("hidden", false);
  18.         $(fileInputContainer).removeClass("mb-2");
  19.         removeBtn.parentElement.remove();
  20.     }
  21. });
  22. $(".upload-new-file-btn").click(function () {
  23.     let hide = !$(this).parent().find(".new-file-input").attr("hidden");
  24.     let fileInputContainer = $(this).parent().find(".new-file-input")[0];
  25.     let fileInput = $(this).parent().find(".new-file-input input")[0];
  26.     let isImage = $(fileInput).attr('accept').indexOf("image") > -1;
  27.     $(fileInputContainer).attr("hidden", hide);
  28.     $(this).parent().find(".file-path").attr("hidden", !hide);
  29.     $(this).text(hide ? (isImage ? "Загрузить новое изображение" : "Загрузить новый файл") : (isImage ? "Не загружать новое изображение" : "Не загружать новый файл"));
  30.     $(this).parent().find(".file-upload-text").html(hide ? "Файл загружен:" : "");
  31.     let removeBtn = Dom.findParentChild($(this)[0], ".remove-file-btn");
  32.     if (removeBtn) {
  33.         $(removeBtn).attr("hidden", !hide);
  34.     }
  35. });
  36. let elementLogicObjects = {};
  37. $('.dropdown').each(function() {
  38.     if ($(this).hasClass('file-dropdown')) {
  39.         let selectFileElement = Dom.findParent($(this)[0], ".select-file");
  40.         if (selectFileElement) {
  41.             let fileDropdown = new FileDropdown(selectFileElement);
  42.             elementLogicObjects[selectFileElement] = fileDropdown;
  43.         }
  44.     } else {
  45.         let dropdownElement = $(this)[0];
  46.         let dropdown = new Dropdown(dropdownElement);
  47.         elementLogicObjects[dropdownElement] = dropdown;
  48.     }
  49. });
  50. $('.dropdown-with-input-filter').each(function() {
  51.     let dropdownWithInputFilterElement = $(this)[0];
  52.     // Найдём вложенный .dropdown внутри контейнера
  53.     let dropdownElement = $(this).find('.dropdown')[0];
  54.     if (!dropdownElement) return;
  55.     let dropdownWithInputFilter = new DropdownWithInputFilter(dropdownElement);
  56.     elementLogicObjects[dropdownWithInputFilterElement] = dropdownWithInputFilter;
  57. });
  58. // Инициализация MonthRangeSliderDropdown
  59. {#$('.month-range-slider-dropdown').each(function() {#}
  60. {#    let container = $(this)[0];#}
  61. {#    let dropdownElement = $(this).find('.dropdown')[0];#}
  62. {#    if (!dropdownElement) return;#}
  63. {#    if (!container._monthRangeSliderDropdownInitialized) {#}
  64. {#        let inst = new MonthRangeSliderDropdown(dropdownElement);#}
  65. {#        elementLogicObjects[container] = inst;#}
  66. {#        container._monthRangeSliderDropdownInitialized = true;#}
  67. {#    }#}
  68. {#});#}
  69. function getElementLogicObject(element) {
  70.     if (element in elementLogicObjects) {
  71.         return elementLogicObjects[element];
  72.     }
  73.     return null;
  74. }
  75. const MODAL_WINDOW_BUTTON = {
  76.     BUTTON_1: "button_1"
  77. }
  78. async function showWindowModal(title, text, buttonText1 = "Yes", buttonText2 = "No",
  79.                                modalDialogStyle = null, onWindowBuild = null) {
  80.     return new Promise((resolve) => {
  81.         // Кешируем селекторы модального окна, чтобы избежать повторных выборок
  82.         const $windowModal = $('#windowModal');
  83.         const $primaryBtn = $windowModal.find('.btn-primary');
  84.         const $secondaryBtn = $windowModal.find('.btn-secondary');
  85.         const $modalDialog = $windowModal.find('.modal-dialog');
  86.         $windowModal.find('.modal-title').text(title);
  87.         $windowModal.find('.modal-body').html(text);
  88.         $primaryBtn.text(buttonText1);
  89.         if (buttonText2 === null) {
  90.             $secondaryBtn.hide();
  91.         } else {
  92.             $secondaryBtn.text(buttonText2);
  93.             $secondaryBtn.show();
  94.         }
  95.         if (modalDialogStyle) {
  96.             $modalDialog.attr('style', modalDialogStyle);
  97.         } else {
  98.             $modalDialog.removeAttr('style');
  99.         }
  100.         $primaryBtn.off('click');
  101.         // Удаляем предыдущие handler'ы и добавляем новые. Используем локальный флаг
  102.         // чтобы гарантировать единственное резолвинг-промиса после завершения анимации скрытия (fade out).
  103.         $windowModal.off('hidden.bs.modal');
  104.         let buttonClicked = false;
  105.         // При клике на primary кнопку — только помечаем, что подтверждена кнопка и прячем модал.
  106.         // Не резолвим здесь; дождёмся события hidden.bs.modal (fade out complete).
  107.         $primaryBtn.on('click', function() {
  108.             buttonClicked = true;
  109.             $windowModal.modal('hide');
  110.         });
  111.         // Слушаем окончание скрытия модального окна (fade out complete).
  112.         $windowModal.on('hidden.bs.modal', function onHidden() {
  113.             // Снимаем обработчик сразу — он уже не нужен.
  114.             $windowModal.off('hidden.bs.modal', onHidden);
  115.             // Резолвим промис по флагу: BUTTON_1 если нажали кнопку, иначе null (отмена/закрытие).
  116.             if (buttonClicked) {
  117.                 resolve(MODAL_WINDOW_BUTTON.BUTTON_1);
  118.             } else {
  119.                 resolve(null);
  120.             }
  121.         });
  122.         if (onWindowBuild && typeof onWindowBuild === 'function') {
  123.             onWindowBuild($windowModal);
  124.         }
  125.         $windowModal.modal('show');
  126.     });
  127. }
  128. const TOAST_TYPE = {
  129.     INFO: "INFO",
  130.     ERROR: "ERROR",
  131.     SUCCESS: "SUCCESS",
  132. }
  133. /**
  134.  *
  135.  * @param {number} duration - длительность показа в мс. -1 для постоянного отображения до закрытия пользователем или программно
  136.  */
  137. function showToast(text, type = TOAST_TYPE.INFO, closeButton = false, duration = 5000) {
  138.     const node = document.createElement('div');
  139.     node.innerHTML = text;
  140.     node.setAttribute("class", "d-inline");
  141.     let data = {
  142.         node: node,
  143.         close: closeButton,
  144.         {#text: text,#}
  145.         duration: duration,
  146.         gravity: "top",
  147.         position: "right",
  148.         // onClick: function () { },
  149.         className: "",
  150.     };
  151.     switch (type) {
  152.         case TOAST_TYPE.ERROR:
  153.             data.style =  {
  154.                 background: "linear-gradient(to right, #ff5f6d, #e65c32ff)",
  155.             }
  156.             break;
  157.         case TOAST_TYPE.SUCCESS:
  158.             data.style =  {
  159.                 background: "linear-gradient(to right, #00b09b, #96c93d)",
  160.             }
  161.             break;
  162.     }
  163.     const toast = Toastify(data);
  164.     toast.showToast();
  165.     return toast;
  166. }
  167. function showTableCompareModal() {
  168.     return new Promise(async (resolve, reject) => {
  169.             const modalContent = `
  170. <div class="form-group">
  171.     <label for="compare-list">Список ФИО (каждое с новой строки):</label>
  172.     <textarea id="compare-list" class="form-control" rows="8"></textarea>
  173. </div>
  174. `.trim();
  175.             let answer = await showWindowModal('Поиск по списку ФИО', modalContent, 'Найти', 'Отмена');
  176.             if (answer === MODAL_WINDOW_BUTTON.BUTTON_1) {
  177.                 resolve({compareList: $('#compare-list').val()});
  178.             } else {
  179.                 resolve(null);
  180.             }
  181.     });
  182. }
  183. function getCompareTableHtml(tableData, compareResult, copyTableColsElementId = null,
  184.                              copyTableFiltersContainerElementSelector = null, dataTableOptions = null) {
  185.     if (!compareResult) {
  186.         return '<div>Нет данных для отображения</div>';
  187.     }
  188.     let compareTableId = 'compare-data-table-' + Date.now();
  189.     let filtersHtml = '';
  190.     let modifiedOptions = null;
  191.     let clonedFiltersContainer = null;
  192.     // Клонирование существующих фильтров (если передан селектор)
  193.     if (copyTableFiltersContainerElementSelector) {
  194.         const $origFilters = $(copyTableFiltersContainerElementSelector);
  195.         if ($origFilters && $origFilters.length) {
  196.             let $clone = $origFilters.clone(true, true);
  197.             const origContainerId = $clone.attr('id');
  198.             if (origContainerId) {
  199.                 $clone.attr('id', 'compare_' + origContainerId);
  200.             }
  201.             $clone.find('[id]').each(function () {
  202.                 const oldId = $(this).attr('id');
  203.                 if (oldId && oldId.indexOf('compare_') !== 0) {
  204.                     $(this).attr('id', 'compare_' + oldId);
  205.                 }
  206.             });
  207.             clonedFiltersContainer = $clone; // сохраним для дальнейшего добавления нового фильтра
  208.             if (dataTableOptions) {
  209.                 try {
  210.                     modifiedOptions = JSON.parse(JSON.stringify(dataTableOptions));
  211.                     if (modifiedOptions.dropdownFiltersConfig && Array.isArray(modifiedOptions.dropdownFiltersConfig.elementsData)) {
  212.                         modifiedOptions.dropdownFiltersConfig.elementsData.forEach(ed => {
  213.                             if (ed.dropdownId) ed.dropdownId = 'compare_' + ed.dropdownId;
  214.                             if (ed.badgeId) ed.badgeId = 'compare_' + ed.badgeId;
  215.                         });
  216.                     }
  217.                 } catch (e) {
  218.                     console.error('Failed to clone dataTableOptions for compare modal', e);
  219.                 }
  220.             }
  221.         }
  222.     }
  223.     // normalize структуры
  224.     const normalize = (res) => {
  225.         if (!res) return { findInOriginal: { foundStrings: [], notFoundStrings: [] }, notFoundOriginal: [], info: {} };
  226.         // already normalized
  227.         if (res.findInOriginal && (res.findInOriginal.foundStrings || res.findInOriginal.notFoundStrings)) {
  228.             return res;
  229.         }
  230.         // old backend format: findOriginalLinesResult + notFoundCompareLines
  231.         if (res.findOriginalLinesResult) {
  232.             const foundStrings = [];
  233.             const notFoundStrings = [];
  234.             const notFoundOriginal = [];
  235.             (res.findOriginalLinesResult || []).forEach(item => {
  236.                 const originalLine = item.originalLine || '';
  237.                 const foundCompareLine = item.foundCompareLine || null;
  238.                 const similarityPercent = (item.similarityPercent !== undefined) ? item.similarityPercent : 0;
  239.                 if (foundCompareLine) {
  240.                     foundStrings.push({ compareLine: foundCompareLine, foundOriginalString: originalLine, similarityPercent: similarityPercent });
  241.                 } else {
  242.                     notFoundOriginal.push(originalLine);
  243.                 }
  244.             });
  245.             (res.notFoundCompareLines || []).forEach(cl => {
  246.                 notFoundStrings.push({ compareLine: cl, similarityPercent: 0 });
  247.             });
  248.             const info = res.info || {};
  249.             return {
  250.                 findInOriginal: { foundStrings, notFoundStrings },
  251.                 notFoundOriginal,
  252.                 info: {
  253.                     originalLinesCount: info.originalLinesCount || 0,
  254.                     compareLinesCount: info.compareLinesCount || 0,
  255.                     foundStringsCount: info.foundCompareLinesCount || foundStrings.length,
  256.                     notFoundStringsCount: info.notFoundCompareLinesCount || notFoundStrings.length,
  257.                     notFoundOriginalCount: (info.notFoundOriginalCount !== undefined) ? info.notFoundOriginalCount : notFoundOriginal.length,
  258.                     foundCompareLinesCount: info.foundCompareLinesCount || 0,
  259.                     foundCompareLinesPartiallyCount: info.foundCompareLinesPartiallyCount || 0,
  260.                     // new counts introduced by updated StringUtils/compareStringArrays
  261.                     foundCompareLinesGoodSimilarityCount: info.foundCompareLinesGoodSimilarityCount || 0,
  262.                     foundCompareLinesBadSimilarityCount: info.foundCompareLinesBadSimilarityCount || 0,
  263.                     minSimilarityPercentage: info.minSimilarityPercentage || 0
  264.                  }
  265.              };
  266.          }
  267.         // fallback: best effort
  268.         const fio = res.findInOriginal || { foundStrings: [], notFoundStrings: [] };
  269.         return {
  270.             findInOriginal: { foundStrings: fio.foundStrings || [], notFoundStrings: fio.notFoundStrings || [] },
  271.             notFoundOriginal: res.notFoundOriginal || [],
  272.             info: Object.assign({
  273.                 originalLinesCount: 0,
  274.                 compareLinesCount: 0,
  275.                 foundStringsCount: (fio.foundStrings || []).length,
  276.                 notFoundStringsCount: (fio.notFoundStrings || []).length,
  277.                 notFoundOriginalCount: (res.notFoundOriginal || []).length
  278.             }, res.info || {})
  279.         };
  280.     };
  281.     const normalized = normalize(compareResult);
  282.     const escapeHtml = (unsafe) => {
  283.         if (unsafe === null || unsafe === undefined) return '';
  284.         return String(unsafe)
  285.             .replace(/&/g, '&amp;')
  286.             .replace(/</g, '&lt;')
  287.             .replace(/>/g, '&gt;')
  288.             .replace(/"/g, '&quot;')
  289.             .replace(/'/g, '&#039;');
  290.     };
  291.     const findInOriginal = normalized.findInOriginal || { foundStrings: [], notFoundStrings: [] };
  292.     const found = findInOriginal.foundStrings || [];
  293.     const notFound = findInOriginal.notFoundStrings || [];
  294.     const notFoundOriginal = normalized.notFoundOriginal || [];
  295.     const info = normalized.info || {};
  296.     const minSimilarity = info.minSimilarityPercentage;
  297.     let html = '';
  298.     // Подготовка исходных колонок
  299.     let srcCols = [];
  300.     let srcRows = [];
  301.     let srcLookup = null;
  302.     const tableDataProvided = Array.isArray(tableData);
  303.     if (tableDataProvided) {
  304.         srcRows = tableData.map(row => {
  305.             if (!Array.isArray(row)) return [];
  306.             return row.map(cell => (cell === null || cell === undefined) ? '' : String(cell).trim());
  307.         });
  308.         // build lookup by column index 1 (as template uses tableData[i][1])
  309.         srcLookup = new Map();
  310.         for (let r = 0; r < srcRows.length; r++) {
  311.             const row = srcRows[r];
  312.             // prefer column index 1; if missing, index 0 or any non-empty
  313.             let key = '';
  314.             if (row.length > 1 && row[1]) {
  315.                 key = row[1];
  316.             } else if (row.length > 0 && row[0]) {
  317.                 key = row[0];
  318.             } else {
  319.                 // find first non-empty
  320.                 for (let c = 0; c < row.length; c++) {
  321.                     if (row[c]) { key = row[c]; break; }
  322.                 }
  323.             }
  324.             if (key) {
  325.                 // if duplicate keys, keep first
  326.                 if (!srcLookup.has(key)) srcLookup.set(key, row);
  327.             }
  328.         }
  329.     }
  330.     if (copyTableColsElementId) {
  331.         const $src = $('#' + copyTableColsElementId);
  332.         if ($src && $src.length) {
  333.             // always read headers into srcCols (we may still use tableData for rows)
  334.             $src.find('thead th').each(function () {
  335.                 srcCols.push(String($(this).text().trim()));
  336.             });
  337.             // read tbody rows from DOM only if tableData was NOT provided
  338.             if (!tableDataProvided) {
  339.                 $src.find('tbody tr').each(function () {
  340.                     const rowArr = [];
  341.                     $(this).find('td').each(function () {
  342.                         rowArr.push(String($(this).text().trim()));
  343.                     });
  344.                     srcRows.push(rowArr);
  345.                 });
  346.             }
  347.          }
  348.      }
  349.     // If no DOM source and no tableData provided, leave srcCols/srcRows empty (matching previous behaviour)
  350.     const compareCols = ['Номер п/п', 'Статус', 'ФИО поиска', 'ФИО таблицы', 'Процент совпадения, %'];
  351.     const finalCols = [];
  352.     srcCols.forEach(c => { if (!finalCols.includes(c)) finalCols.push(c); });
  353.     compareCols.forEach(c => { if (!finalCols.includes(c)) finalCols.push(c); });
  354.     // --- Новый фильтр "Статус поиска" ---
  355.     const statusColIndex = finalCols.indexOf('Статус');
  356.     const statusFilterDropdownId = 'compareStatusDropdown_' + compareTableId;
  357.     // HTML нового фильтра
  358.     const statusFilterHtml = `
  359. <div class="dropdown-filter">
  360.   <label class="form-label">Статус поиска ФИО:</label>
  361.   <div class="dropdown">
  362.     <button class="btn btn-primary dropdown-toggle" type="button" id="${statusFilterDropdownId}" data-toggle="dropdown" aria-expanded="false" data-multiple="false">Все статусы</button>
  363.     <div class="dropdown-menu" aria-labelledby="${statusFilterDropdownId}">
  364.       <a class="dropdown-item" href="#" data-filter="all">Все статусы</a>
  365.       <div class="dropdown-divider"></div>
  366.       <!-- Dynamic items will be added here -->
  367.     </div>
  368.   </div>
  369. </div>`;
  370.     if (clonedFiltersContainer) {
  371.         // Добавляем новый фильтр в конец контейнера
  372.         clonedFiltersContainer.append($(statusFilterHtml));
  373.         filtersHtml = $('<div>').append(clonedFiltersContainer).html();
  374.     } else {
  375.         // Создаем новый контейнер только с фильтром статуса
  376.         filtersHtml = `<div id="compare-filters-${compareTableId}" class="mb-3 d-flex align-items-center" style="gap:20px; flex-wrap:wrap;">${statusFilterHtml}</div>`;
  377.     }
  378.     // Обновляем / создаём конфиг options для CustomDataTable
  379.     if (!modifiedOptions) {
  380.         modifiedOptions = {
  381.             dropdownFiltersConfig: { elementsData: [] },
  382.             drawRowNumbersForColIndex: 0,
  383.             lengthMenu: [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'Все']]
  384.         };
  385.     }
  386.     if (!modifiedOptions.dropdownFiltersConfig) {
  387.         modifiedOptions.dropdownFiltersConfig = { elementsData: [] };
  388.     }
  389.     if (!Array.isArray(modifiedOptions.dropdownFiltersConfig.elementsData)) {
  390.         modifiedOptions.dropdownFiltersConfig.elementsData = [];
  391.     }
  392.     if (statusColIndex !== -1) {
  393.         modifiedOptions.dropdownFiltersConfig.elementsData.push({
  394.             columnIndex: statusColIndex,
  395.             dropdownId: statusFilterDropdownId,
  396.             badgeId: null,
  397.             textForAll: 'Все статусы'
  398.         });
  399.     }
  400.     // Вставляем фильтры перед сводкой
  401.     if (filtersHtml) {
  402.         html += '<div class="mb-3">' + filtersHtml + '</div>';
  403.     }
  404.     // Сводка
  405.     html += '<div class="mb-3">';
  406.     html += '<strong>Всего в таблице:</strong> ' + escapeHtml(info.originalLinesCount || 0) + '<br/>';
  407.     html += '<strong>Всего в списке поиска:</strong> ' + escapeHtml(info.compareLinesCount || 0) + '<br/>';
  408.     html += '<strong>Найдено совпадений:</strong> ' + escapeHtml(info.foundStringsCount || found.length) + '<br/>';
  409.     html += '<strong>Частично найдено (более ' + minSimilarity + '% совпадения):</strong> ' + (info.foundCompareLinesGoodSimilarityCount || 0) + '<br/>';
  410.     html += '<strong>Частично найдено (до ' + minSimilarity + '% совпадения):</strong> ' + (info.foundCompareLinesBadSimilarityCount || 0) + '<br/>';
  411.     html += '<strong>Частично найдено всего:</strong> ' + ((info.foundCompareLinesBadSimilarityCount + info.foundCompareLinesGoodSimilarityCount) || 0) + '<br/>';
  412.     html += '<strong>Не найдено в таблице:</strong> ' + notFound.length + '<br/>';
  413.     html += '<strong>Не найдено в списке поиска:</strong> ' + (info.notFoundOriginalCount || notFoundOriginal.length) + '<br/>';
  414.     html += '</div>';
  415.     html += '<h6>Результаты поиска</h6>';
  416.     // Кнопка скачивания CSV
  417.     const downloadBtnId = 'download-csv-btn-' + compareTableId;
  418.     html += '<div class="mb-2 d-flex justify-content-end">';
  419.     html += '<button id="' + downloadBtnId + '" type="button" class="btn btn-secondary btn-sm">Скачать таблицу</button>';
  420.     html += '</div>';
  421.     html += '<div class="table-responsive"><table id="' + compareTableId + '" class="table table-sm table-bordered">';
  422.     html += '<thead><tr>';
  423.     finalCols.forEach(col => { html += '<th>' + col + '</th>'; });
  424.     html += '</tr></thead><tbody>';
  425.     let rowIndex = 0;
  426.     const findSrcRowByValue = (value) => {
  427.         if (!value) return null;
  428.         const valNorm = String(value).trim();
  429.         // if we have a lookup built from tableData, use it and return only rows from tableData
  430.         if (srcLookup) {
  431.             if (srcLookup.has(valNorm)) return srcLookup.get(valNorm);
  432.             return null; // do NOT search DOM or other columns when tableData provided
  433.         }
  434.         for (let r = 0; r < srcRows.length; r++) {
  435.             for (let c = 0; c < srcRows[r].length; c++) {
  436.                 if (srcRows[r][c] === valNorm) return srcRows[r];
  437.             }
  438.         }
  439.         return null;
  440.     };
  441.     const renderRow = (cells, extraClass) => {
  442.         html += '<tr' + (extraClass ? ' class="' + extraClass + '"' : '') + '>';
  443.         finalCols.forEach(fc => {
  444.             const val = (fc in cells) ? cells[fc] : '';
  445.             html += '<td>' + (val === null || val === undefined ? '' : val) + '</td>';
  446.         });
  447.         html += '</tr>';
  448.     };
  449.     const mapSrcRowToObj = (srcRowArr) => {
  450.         const obj = {};
  451.         srcCols.forEach((h, idx) => { obj[h] = srcRowArr[idx] || ''; });
  452.         return obj;
  453.     };
  454.     if (tableDataProvided && srcRows.length) {
  455.         // build map from original value -> found item
  456.         const foundByOriginal = new Map();
  457.         found.forEach(it => {
  458.             const key = (it.foundOriginalString || '').trim();
  459.             if (key) foundByOriginal.set(key, it);
  460.         });
  461.         // iterate original rows in order
  462.         for (let r = 0; r < srcRows.length; r++) {
  463.             const srcRow = srcRows[r];
  464.             // derive key from same logic as srcLookup
  465.             let key = '';
  466.             if (srcRow.length > 1 && srcRow[1]) key = srcRow[1];
  467.             else if (srcRow.length > 0 && srcRow[0]) key = srcRow[0];
  468.             else {
  469.                 for (let c = 0; c < srcRow.length; c++) { if (srcRow[c]) { key = srcRow[c]; break; } }
  470.             }
  471.             rowIndex++;
  472.             const cells = mapSrcRowToObj(srcRow);
  473.             const foundItem = key ? foundByOriginal.get(key) : null;
  474.             if (foundItem) {
  475.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  476.                 const pct = (foundItem.similarityPercent !== undefined && foundItem.similarityPercent !== null) ? Number(foundItem.similarityPercent) : 0;
  477.                 let statusText = '';
  478.                 if (pct === 100) {
  479.                     statusText = 'Найдено';
  480.                 } else if (pct > minSimilarity) {
  481.                     statusText = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  482.                 } else {
  483.                     statusText = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  484.                 }
  485.                 cells['Статус'] = statusText;
  486.                 cells['ФИО поиска'] = escapeHtml(foundItem.compareLine);
  487.                 cells['ФИО таблицы'] = escapeHtml(foundItem.foundOriginalString || key);
  488.                 cells['Процент совпадения, %'] = (foundItem.similarityPercent !== undefined && foundItem.similarityPercent !== null) ? escapeHtml(foundItem.similarityPercent) : '';
  489.                 const rowClass = (pct === 100) ? 'table-success' : 'table-warning';
  490.                 renderRow(cells, rowClass);
  491.                 foundByOriginal.delete(key);
  492.             } else {
  493.                 // red row - not found in compare
  494.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  495.                 cells['Статус'] = 'Не найдено в списке поиска';
  496.                 cells['ФИО поиска'] = '';
  497.                 cells['ФИО таблицы'] = escapeHtml(key);
  498.                 cells['Процент совпадения, %'] = '';
  499.                 renderRow(cells, 'table-danger');
  500.             }
  501.         }
  502.         // render remaining compare lines that weren't matched to any original
  503.         // these are in notFound OR leftover in foundByOriginal if any
  504.         // first render compare items from notFound (explicitly unmatched)
  505.         notFound.forEach(item => {
  506.             rowIndex++;
  507.             let cells = {};
  508.             cells['Номер п/п'] = escapeHtml(rowIndex);
  509.             cells['Статус'] = 'Не найдено в таблице';
  510.             cells['ФИО поиска'] = escapeHtml(item.compareLine);
  511.             cells['ФИО таблицы'] = '';
  512.             cells['Процент совпадения, %'] = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  513.             // use blue for notFound compare lines
  514.             renderRow(cells, 'table-info');
  515.         });
  516.         // any leftover found items that weren't matched by key (edge cases)
  517.         if (foundByOriginal.size) {
  518.             foundByOriginal.forEach(it => {
  519.                 rowIndex++;
  520.                 let cells = {};
  521.                 cells['Номер п/п'] = escapeHtml(rowIndex);
  522.                 const pct2 = (it.similarityPercent !== undefined && it.similarityPercent !== null) ? Number(it.similarityPercent) : 0;
  523.                 let statusText2 = '';
  524.                 if (pct2 === 100) {
  525.                     statusText2 = 'Найдено';
  526.                 } else if (pct2 > minSimilarity) {
  527.                     statusText2 = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  528.                 } else {
  529.                     statusText2 = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  530.                 }
  531.                 cells['Статус'] = statusText2;
  532.                 cells['ФИО поиска'] = escapeHtml(it.compareLine);
  533.                 cells['ФИО таблицы'] = escapeHtml(it.foundOriginalString || '');
  534.                 cells['Процент совпадения, %'] = (it.similarityPercent !== undefined && it.similarityPercent !== null) ? escapeHtml(it.similarityPercent) : '';
  535.                 const rowClass2 = (pct2 === 100) ? 'table-success' : 'table-warning';
  536.                 renderRow(cells, rowClass2);
  537.             });
  538.         }
  539.     } else {
  540.         // fallback original behaviour: show found, then notFound, then notFoundOriginal
  541.         found.forEach(item => {
  542.             rowIndex++;
  543.             const compareString = escapeHtml(item.compareLine);
  544.             const foundOriginal = escapeHtml(item.foundOriginalString);
  545.             const percent = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  546.             let cells = {};
  547.             if (srcCols.length && foundOriginal) {
  548.                 const srcRow = findSrcRowByValue(foundOriginal);
  549.                 if (srcRow) Object.assign(cells, mapSrcRowToObj(srcRow));
  550.             }
  551.             const pctItem = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? Number(item.similarityPercent) : 0;
  552.             let statusTextItem = '';
  553.             if (pctItem === 100) {
  554.                 statusTextItem = 'Найдено';
  555.             } else if (pctItem > minSimilarity) {
  556.                 statusTextItem = 'Частично найден в таблице - более ' + minSimilarity + '% совпадение';
  557.             } else {
  558.                 statusTextItem = 'Частично найден в таблице - менее ' + minSimilarity + '% совпадение';
  559.             }
  560.             cells['Номер п/п'] = escapeHtml(rowIndex);
  561.             cells['Статус'] = statusTextItem;
  562.             cells['ФИО поиска'] = compareString;
  563.             cells['ФИО таблицы'] = foundOriginal;
  564.             cells['Процент совпадения, %'] = percent;
  565.             const rowClassItem = (pctItem === 100) ? 'table-success' : 'table-warning';
  566.             renderRow(cells, rowClassItem);
  567.         });
  568.          notFound.forEach(item => {
  569.              rowIndex++;
  570.              const compareString = escapeHtml(item.compareLine);
  571.              const percent = (item.similarityPercent !== undefined && item.similarityPercent !== null) ? escapeHtml(item.similarityPercent) : '';
  572.              let cells = {};
  573.              cells['Номер п/п'] = escapeHtml(rowIndex);
  574.              cells['Статус'] = 'Не найдено в таблице';
  575.              cells['ФИО поиска'] = compareString;
  576.              cells['ФИО таблицы'] = '';
  577.              cells['Процент совпадения, %'] = percent;
  578.              renderRow(cells, 'table-danger');
  579.          });
  580.          notFoundOriginal.forEach(item => {
  581.              rowIndex++;
  582.              const original = escapeHtml(item);
  583.              let cells = {};
  584.              if (srcCols.length) {
  585.                  const srcRow = findSrcRowByValue(item);
  586.                  if (srcRow) Object.assign(cells, mapSrcRowToObj(srcRow));
  587.              }
  588.              cells['Номер п/п'] = escapeHtml(rowIndex);
  589.              cells['Статус'] = 'Не найдено в списке поиска';
  590.              cells['ФИО поиска'] = '';
  591.              cells['ФИО таблицы'] = original;
  592.              cells['Процент совпадения, %'] = '';
  593.              renderRow(cells, 'table-secondary');
  594.          });
  595.      }
  596.     // --- New: ensure initialization script with modifiedOptions ---
  597.     if (modifiedOptions) {
  598.         if (modifiedOptions.drawRowNumbersForColIndex === undefined) {
  599.             modifiedOptions.drawRowNumbersForColIndex = 0;
  600.         }
  601.         html += '</tbody></table></div>';
  602.         html += '<script>(function(){try{var opts=' + JSON.stringify(modifiedOptions) + ';var table=new CustomDataTable("#' + compareTableId + '",opts);table.initDataTable();var btn=document.getElementById("' + downloadBtnId + '");if(btn){btn.addEventListener("click",function(){table.downloadCsv("compare_table_export.csv");});}}catch(e){console.error("Failed to initialize compare CustomDataTable",e);}})();</'+'script>';
  603.     } else {
  604.         html += '</tbody></table></div>';
  605.     }
  606.     return html;
  607.  }
  608. function compareData(data1, data2) {
  609.     return new Promise((resolve, reject) => {
  610.         Utils.fetch(API_COMPARE_DATA_URL, 'POST', { data1: data1, data2: data2 })
  611.             .then(async (response) => {
  612.                 console.log(response);
  613.                 resolve(response.response);
  614.             })
  615.             .catch((response) => {
  616.                 showToast("Ошибка. Не удалось провести сравнение. Попробуйте повторить позднее.",
  617.                     TOAST_TYPE.ERROR);
  618.                 console.log(response);
  619.                 reject(response);
  620.             });
  621.     });
  622. }
  623. function updateDependentElementVisibility(controllerSelector, $dependent) {
  624.     const $controllerElement = $(controllerSelector);
  625.     let optionAttrName = $dependent.data('controlling-select-option-attr-name');
  626.     let selectValues = $dependent.data('controlling-element-value');
  627.     if (selectValues !== null && selectValues !== undefined) {
  628.         if (!Array.isArray(selectValues)) {
  629.             selectValues = [selectValues];
  630.         }
  631.     }
  632.     let excludeValues = $dependent.data('controlling-element-value-exclude');
  633.     if (excludeValues !== null && excludeValues !== undefined) {
  634.         if (!Array.isArray(excludeValues)) {
  635.             excludeValues = [excludeValues];
  636.         }
  637.     }
  638.     const controllingElementTag = $controllerElement.prop('tagName').toLowerCase();
  639.     let selectedControllerVal = null;
  640.     switch (controllingElementTag) {
  641.         case 'input':
  642.             selectedControllerVal = !!$controllerElement.is(':checked');
  643.             break;
  644.         default:
  645.             selectedControllerVal = $controllerElement.val();
  646.     }
  647.     if (optionAttrName) {
  648.         $dependent.find('option').each(function () {
  649.             let $opt = $(this);
  650.             let optAttrVal = $opt.attr(optionAttrName);
  651.             if (optAttrVal == selectedControllerVal || $opt.val() == -1) {
  652.                 $opt.show();
  653.             } else {
  654.                 $opt.hide();
  655.             }
  656.         });
  657.         if ($dependent.find('option:selected').attr(optionAttrName) != selectedControllerVal) {
  658.             $dependent.val(-1);
  659.         }
  660.     } else if (selectValues || excludeValues) {
  661.         let isExcluded = excludeValues ? excludeValues.includes(selectedControllerVal) : false;
  662.         let isIncluded = selectValues ? selectValues.includes(selectedControllerVal) : true;
  663.         switch (controllingElementTag) {
  664.             case 'input':
  665.             case 'select':
  666.                 $dependent.toggle(!isExcluded && isIncluded);
  667.                 break;
  668.             default:
  669.                 throw new Error('Unsupported controlling element tag for data-controlling-element-value: ' + controllingElementTag);
  670.         }
  671.     }
  672. }
  673. function bindDependentElementVisibility(element) {
  674.         let $dependent = $(element);
  675.         let controllerSelector = $dependent.data('controlling-element-selector');
  676.         if (!controllerSelector) return;
  677.         $(controllerSelector).on('change', function() {
  678.             updateDependentElementVisibility(controllerSelector, $dependent);
  679.         });
  680.         if ($(controllerSelector).length) {
  681.             updateDependentElementVisibility(controllerSelector, $dependent);
  682.         }
  683. }
  684. $('[data-controlling-element-selector]').each(function() {
  685.     bindDependentElementVisibility($(this)[0]);
  686. });
  687. function updateCheckAllMasterState($master, targetSelector) {
  688.     try {
  689.         const $targets = $(targetSelector).filter(':not(:disabled)');
  690.         const total = $targets.length;
  691.         if (total === 0) {
  692.             $master.prop('checked', false);
  693.             $master.prop('indeterminate', false);
  694.             return;
  695.         }
  696.         const checkedCount = $targets.filter(':checked').length;
  697.         if (checkedCount === 0) {
  698.             $master.prop('checked', false);
  699.             $master.prop('indeterminate', false);
  700.         } else if (checkedCount === total) {
  701.             $master.prop('checked', true);
  702.             $master.prop('indeterminate', false);
  703.         } else {
  704.             $master.prop('checked', false);
  705.             $master.prop('indeterminate', true);
  706.         }
  707.     } catch (e) {
  708.         console.error('updateCheckAllMasterState error', e);
  709.     }
  710. }
  711. function bindCheckAll(masterCheckbox) {
  712.     let $master = $(masterCheckbox);
  713.     let targetSelector = $master.attr('data-target') || $master.data('target');
  714.     if (!targetSelector) return;
  715.     // When master changes, set all targets checked state
  716.     $master.off('change.checkAll').on('change.checkAll', function() {
  717.         try {
  718.             const checked = $master.is(':checked');
  719.             $(targetSelector).each(function () {
  720.                 // only change inputs of type checkbox or radios
  721.                 const $t = $(this);
  722.                 if ($t.is(':disabled')) return;
  723.                 if ($t.is(':checkbox') || $t.is(':radio') || $t.attr('type') === 'checkbox' || $t.attr('type') === 'radio') {
  724.                     $t.prop('checked', checked);
  725.                     // trigger change so other logic can react
  726.                     $t.trigger('change');
  727.                 }
  728.             });
  729.         } catch (e) {
  730.             console.error('bindCheckAll master change handler error', e);
  731.         }
  732.     });
  733.     // When any target changes, update master state
  734.     // Use delegated handler to avoid multiple bindings; namespace with selector
  735.     const delegatedEventName = 'change.checkAll.' + encodeURIComponent(targetSelector);
  736.     // remove possible existing delegated handlers for same selector to avoid duplication
  737.     $(document).off(delegatedEventName, targetSelector);
  738.     $(document).on(delegatedEventName, targetSelector, function() {
  739.         updateCheckAllMasterState($master, targetSelector);
  740.     });
  741.     // initialize master state
  742.     updateCheckAllMasterState($master, targetSelector);
  743. }
  744. let keysData = {
  745.     shift: { isPressed: false }
  746. };
  747. function bindWindowKeyPressHandling() {
  748.     // remove previous handlers in the same namespace to avoid double binding
  749.     {#$(window).off('.shiftCheck');#}
  750.     // keydown/keyup handlers: support modern `e.key` and legacy `keyCode/which`.
  751.     $(window).on('keydown.shiftCheck', function(e) {
  752.         const key = e.key || (e.originalEvent && e.originalEvent.key) || null;
  753.         const code = e.keyCode || e.which || (e.originalEvent && (e.originalEvent.keyCode || e.originalEvent.which));
  754.         if (key === 'Shift' || code === 16) {
  755.             keysData.shift.isPressed = true;
  756.         }
  757.     });
  758.     $(window).on('keyup.shiftCheck', function(e) {
  759.         const key = e.key || (e.originalEvent && e.originalEvent.key) || null;
  760.         const code = e.keyCode || e.which || (e.originalEvent && (e.originalEvent.keyCode || e.originalEvent.which));
  761.         if (key === 'Shift' || code === 16) {
  762.             keysData.shift.isPressed = false;
  763.         }
  764.     });
  765.     // Reset modifier state on window blur to avoid "stuck" state when focus is lost
  766.     $(window).on('blur.shiftCheck', function() {
  767.         keysData.shift.isPressed = false;
  768.     });
  769. }
  770. // Immediately bind key handlers so other logic that relies on `keysData.shift.isPressed` works
  771. bindWindowKeyPressHandling();
  772. $('[data-toggle="check-all"]').each(function() {
  773.     bindCheckAll(this);
  774. });
  775. function bindSetCheckedDownToOfContainer(container) {
  776.     let muteInputChangeEvents = false;
  777.     $(container).find('input[type="checkbox"]').on('change', function() {
  778.         if (muteInputChangeEvents) {
  779.             return;
  780.         }
  781.         if (!keysData.shift.isPressed) {
  782.             return;
  783.         }
  784.         let $lastInput = $(this);
  785.         muteInputChangeEvents = true;
  786.         $(container).find('input[type="checkbox"]').each(function() {
  787.             if (this === $lastInput[0]) {
  788.                 return false;
  789.             }
  790.             $(this).prop('checked', $lastInput.is(':checked'));
  791.         });
  792.         muteInputChangeEvents = false;
  793.     });
  794. }
  795. $('[data-set-checked-down-to-container]').each(function() {
  796.     bindSetCheckedDownToOfContainer(this);
  797. });
  798. $('[data-toggle="count-text"]').each(function() {
  799.     let $el = $(this);
  800.     let $target = $($el.data('target'));
  801.     let template = $el.data('count-text-template');
  802.     function update() {
  803.         let count = $el.find('option:selected').length;
  804.         $target.html(count ? template.replace('{selected-count}', count) : '');
  805.     }
  806.     $el.on('change', update);
  807.     update();
  808. });
  809. function createEntityCollection(element) {
  810.     let ec = new EntityCollection(element);
  811.     elementLogicObjects[element] = ec;
  812. }
  813. $('[data-init="entity-collection"]').each(function() {
  814.     createEntityCollection($(this)[0]);
  815. });
  816. function wrapTextInTag(textarea, tag) {
  817.     let start = textarea.selectionStart;
  818.     let end = textarea.selectionEnd;
  819.     let selectedText = textarea.value.substring(start, end);
  820.     if (selectedText) {
  821.         let openTag = '<' + tag + '>';
  822.         let closeTag = '</' + tag + '>';
  823.         let textBefore = textarea.value.substring(start - openTag.length, start);
  824.         let textAfter = textarea.value.substring(end, end + closeTag.length);
  825.         if (textBefore === openTag && textAfter === closeTag) {
  826.             textarea.value = textarea.value.substring(0, start - openTag.length) + selectedText + textarea.value.substring(end + closeTag.length);
  827.             let newStart = start - openTag.length;
  828.             let newEnd = newStart + selectedText.length;
  829.             textarea.focus();
  830.             textarea.setSelectionRange(newStart, newEnd);
  831.         } else {
  832.             let wrappedText = openTag + selectedText + closeTag;
  833.             textarea.value = textarea.value.substring(0, start) + wrappedText + textarea.value.substring(end);
  834.             textarea.focus();
  835.             textarea.setSelectionRange(start + openTag.length, start + openTag.length + selectedText.length);
  836.         }
  837.         $(textarea).trigger('change');
  838.     }
  839. }
  840. function bindTextEditButton(element) {
  841.     let $button = $(element);
  842.     let $formGroup = $button.closest('.form-group');
  843.     let $textElement = $formGroup.find(`div[contenteditable="true"]`);
  844.     $button.off('click').on('click', function() {
  845.         if ($textElement.length) {
  846.             if ($button.hasClass('bold-text-button')) {
  847.                 document.execCommand('bold');
  848.             } else if ($button.hasClass('italic-text-button')) {
  849.                 document.execCommand('italic');
  850.             } else if ($button.hasClass('underline-text-button')) {
  851.                 document.execCommand('underline');
  852.             }
  853.         }
  854.     });
  855. }
  856. $('[data-dom-listener-groups*="textEditButton"]').each(function() {
  857.     bindTextEditButton($(this)[0]);
  858. });
  859. function insertEmoji(editableDiv, emoji) {
  860.     if (!editableDiv) return;
  861.     // support for textarea / input
  862.     const tagName = (editableDiv.tagName || '').toLowerCase();
  863.     if (tagName === 'textarea' || tagName === 'input') {
  864.         try {
  865.             const start = typeof editableDiv.selectionStart === 'number' ? editableDiv.selectionStart : 0;
  866.             const end = typeof editableDiv.selectionEnd === 'number' ? editableDiv.selectionEnd : start;
  867.             const value = editableDiv.value || '';
  868.             const newValue = value.substring(0, start) + emoji + value.substring(end);
  869.             editableDiv.value = newValue;
  870.             const pos = start + emoji.length;
  871.             editableDiv.focus();
  872.             try { editableDiv.setSelectionRange(pos, pos); } catch (e) { /* ignore */ }
  873.             $(editableDiv).trigger('input').trigger('change');
  874.         } catch (e) {
  875.             console.error('insertEmoji (input) error', e);
  876.         }
  877.         return;
  878.     }
  879.     // support for contenteditable elements
  880.     try {
  881.         // ensure element is focused
  882.         editableDiv.focus();
  883.         const sel = window.getSelection();
  884.         if (!sel) return;
  885.         let range = null;
  886.         if (sel.rangeCount > 0) {
  887.             range = sel.getRangeAt(0).cloneRange();
  888.         }
  889.         // If there's no range or the current range is outside editableDiv, set caret to end
  890.         let isInside = false;
  891.         if (range) {
  892.             const container = range.commonAncestorContainer;
  893.             isInside = editableDiv.contains(container) || container === editableDiv;
  894.         }
  895.         if (!range || !isInside) {
  896.             range = document.createRange();
  897.             range.selectNodeContents(editableDiv);
  898.             range.collapse(false); // to end
  899.         }
  900.         // Delete current selection (if any)
  901.         range.deleteContents();
  902.         // Insert emoji as a text node
  903.         const textNode = document.createTextNode(emoji);
  904.         range.insertNode(textNode);
  905.         // Move caret after inserted node
  906.         range.setStartAfter(textNode);
  907.         range.collapse(true);
  908.         // Apply the new range
  909.         sel.removeAllRanges();
  910.         sel.addRange(range);
  911.         // Trigger input/change so other bindings (that sync to textarea) update
  912.         $(editableDiv).trigger('input').trigger('change');
  913.     } catch (e) {
  914.         console.error('insertEmoji (contenteditable) error', e);
  915.     }
  916. }
  917. function bindEmojiButton(element) {
  918.     let $button = $(element);
  919.     let $formGroup = $button.closest('.form-group');
  920.     let $textElement = $formGroup.find(`div[contenteditable="true"]`);
  921.     $button.off('click').on('click', function() {
  922.         let emoji = $button.data("insert-text") ? $button.data("insert-text") : $button.text();
  923.         emoji = emoji.trim()
  924.         insertEmoji($textElement[0], emoji);
  925.     });
  926. }
  927. $('[data-dom-listener-groups*="emojiButton"]').each(function() {
  928.     bindEmojiButton($(this)[0]);
  929. });
  930. function bindTextEdit(element) {
  931.     let $formGroup = $(element).closest('.form-group');
  932.     let $textarea = $formGroup.find(`textarea`);
  933.     $(element).on('input', () => {
  934.         $(element).find('font').each(function () {
  935.             $(this).removeAttr('face').removeAttr('color').replaceWith($(this).contents());
  936.         });
  937.         $(element).find('*').each(function () {
  938.             const style = $(this).attr('style');
  939.             if (style) {
  940.                 const newStyle = style.replace(/color\s*:\s*[^;]+;?/gi, '').trim();
  941.                 if (newStyle) {
  942.                     $(this).attr('style', newStyle);
  943.                 } else {
  944.                     $(this).removeAttr('style');
  945.                 }
  946.             }
  947.         });
  948.         $textarea.val($(element).html()).trigger('change');
  949.     });
  950. }
  951. $('[data-dom-listener-groups*="textEdit"]').each(function() {
  952.     bindTextEdit($(this)[0]);
  953. });
  954. function autoResizeTextarea(textarea) {
  955.     let scrollX = window.scrollX;
  956.     let scrollY = window.scrollY;
  957.     textarea.style.height = '0';
  958.     textarea.style.height = textarea.scrollHeight + 'px';
  959.     window.scrollTo({ left: scrollX, top: scrollY, behavior: 'instant' });
  960. }
  961. function bindAutoResizeTextarea(element) {
  962.     let $textarea = $(element);
  963.     $textarea.css({ overflow: 'hidden', resize: 'vertical', 'min-height': '80px' });
  964.     $textarea.off('input.autoResize').on('input.autoResize', function() {
  965.         autoResizeTextarea(this);
  966.     });
  967.     autoResizeTextarea(element);
  968. }
  969. $('[data-toggle="auto-resize-textarea"]').each(function() {
  970.     bindAutoResizeTextarea(this);
  971. });
  972. domDispatcher.eventDispatcher.on(
  973.     DomDispatcher.EVENTS.ON_DOM_NODE_ADDED,
  974.     (data, event) => {
  975.         if (data.groupNames.includes("dependentElementVisibility")) {
  976.             bindDependentElementVisibility(data.element);
  977.         } else if (data.groupNames.includes("entityCollection")) {
  978.             createEntityCollection(data.element);
  979.         } else if (data.groupNames.includes("textEditButton")) {
  980.             bindTextEditButton(data.element);
  981.         } else if (data.groupNames.includes("emojiButton")) {
  982.             bindEmojiButton(data.element);
  983.         } else if (data.groupNames.includes("autoResizeTextarea")) {
  984.             bindAutoResizeTextarea(data.element);
  985.         } else if (data.groupNames.includes("textEdit")) {
  986.             bindTextEdit(data.element);
  987.         }
  988.     }, {
  989.         groupNames: ["dependentElementVisibility", "entityCollection", "textEditButton",
  990.             "emojiButton", "autoResizeTextarea", "textEdit"],
  991. });
  992. /** @type {DateRangePickerWithButton} */
  993. let calendarEventsSelectorComponent = null;
  994. {#initBearer.wait([#}
  995. {#    INIT_BEARERS.GLOBAL_USER_SETTINGS_MODIFIER,#}
  996. {#    INIT_BEARERS.GLOBAL_USER_SETTINGS_FORM_SAVER,#}
  997. {#]).then(() => {#}
  998. {#    let monthSliderRangeDropdownElement = $('.month-range-slider-dropdown .dropdown').first()[0];#}
  999. {#    monthRangeSliderRangeDropdown =#}
  1000. {#        new MonthRangeSliderDropdown(monthSliderRangeDropdownElement, {#}
  1001. {#            monthRangeSliderOptions: {#}
  1002. {#                minDate: new Date("2025-01-01") ,#}
  1003. {#                maxDate:  new Date(new Date().getFullYear() + 1, 11, 31),#}
  1004. {#                startDate: new Date(userSettings.startCalendarEventsDate),#}
  1005. {#                endDate: new Date(userSettings.endCalendarEventsDate),#}
  1006. {#            }#}
  1007. {#        });#}
  1008. {#    userSettingsModifier.bindMonthRangeSliderDropdown('startCalendarEventsDate',#}
  1009. {#        'endCalendarEventsDate', monthRangeSliderRangeDropdown, false);#}
  1010. {#    userSettingsFormSaver.eventDispatcher.on(userSettingsFormSaver.EVENTS.ON_SAVE, () => {#}
  1011. {#        window.location.reload();#}
  1012. {#    });#}
  1013. {#    initBearer.set(INIT_BEARERS.GLOBAL_MONTH_RANGE_SLIDER_RANGE_DROPDOWN,#}
  1014. {#        monthRangeSliderRangeDropdown);#}
  1015. {#});#}
  1016. initBearer.wait([
  1017.     INIT_BEARERS.GLOBAL_USER_SETTINGS_MODIFIER,
  1018.     INIT_BEARERS.GLOBAL_USER_SETTINGS_FORM_SAVER,
  1019. ]).then(() => {
  1020.     let element = $('.date-range-picker-with-button')[0];
  1021.     if (element) {
  1022.         calendarEventsSelectorComponent = new DateRangePickerWithButton(element, {
  1023.             startDate: new Date(userSettings.startCalendarEventsDate),
  1024.             endDate: new Date(userSettings.endCalendarEventsDate),
  1025.             datePickerOptions: {
  1026.                 rangeLabels: {
  1027.                     TODAY: 'За сегодня',
  1028.                     YESTERDAY: 'За вчера',
  1029.                     LAST_7_DAYS: 'За последние 7 дней',
  1030.                     LAST_30_DAYS: 'За последние 30 дней',
  1031.                     THIS_MONTH: 'За этот месяц',
  1032.                     LAST_MONTH: 'За прошлый месяц',
  1033.                     CURRENT_AND_FUTURE: 'Текущие и будущие',
  1034.                     ALL_TIME: 'За всё время',
  1035.                 }
  1036.             }
  1037.         });
  1038.         userSettingsModifier.bindDateRangePickerWithButton('startCalendarEventsDate',
  1039.             'endCalendarEventsDate', calendarEventsSelectorComponent, false, "calendarEventsDateRangeName");
  1040.         userSettingsFormSaver.eventDispatcher.on(userSettingsFormSaver.EVENTS.ON_SAVE, () => {
  1041.             window.location.reload();
  1042.         });
  1043.         initBearer.set(INIT_BEARERS.GLOBAL_CALENDAR_EVENTS_SELECTOR_COMPONENT,
  1044.             calendarEventsSelectorComponent);
  1045.     }
  1046. });
  1047. //todo redo
  1048. $(".new-item-text").on("input", function() {
  1049.     $closestList = $(this).parent().parent().parent().find("ul.dropdown-menu");
  1050.     $newItem = $closestList.find("li > a[data-list-item-id=-100]").parent();
  1051.     $newItem.find("a").text($(this).val() ? " " + $(this).val() : "–");
  1052.     $newItem.toggle($(this).val().trim().length > 0);
  1053. });
  1054. {#initBearer.wait([INIT_BEARERS.GLOBAL_CALENDAR_EVENTS_SELECTOR_COMPONENT]).then(() => {#}
  1055. {#    let monthRangeSliderTipToast = null;#}
  1056. {#    calendarEventsSelectorComponent.eventDispatcher.on(#}
  1057. {#        calendarEventsSelectorComponent.EVENTS.ON_DROPDOWN_SHOW, () => { #}{# for MonthRangeSliderDropdown #}
  1058. {#            let toastMsg = `Подсказка<br><br>#}
  1059. {#Для изменения периода мероприятий:<br><br>#}
  1060. {#<b>1</b>. Измените положение <div class="month-range-thumb month-range-thumb-start" data-thumb="start"#}
  1061. {#style="position: static; display: inline-block; transform: translate(0, 5px);"></div> <b>красного</b>#}
  1062. {#и/или <div class="month-range-thumb month-range-thumb-end" data-thumb="end"#}
  1063. {#style="position: static; display: inline-block; transform: translate(0, 5px);"></div> <b>синего</b><br>#}
  1064. {#ползунков с помощью мыши, потянув<br>#}
  1065. {#их влево или вправо.<br><br>#}
  1066. {#<b>2</b>. Нажмите кнопку<br>#}
  1067. {#«Применить».#}
  1068. {#<br><br>#}
  1069. {#<label><input type="checkbox"#}
  1070. {#    data-dom-listener-groups='["userTipSettingsModifier"]'#}
  1071. {#    data-json-to-component-bind-key-name="showCalendarEventsPeriodTip"#}
  1072. {#> Не показывать эту подсказку в дальнейшем</label>#}
  1073. {#<br>#}
  1074. {#`;#}
  1075. {#            if (userTipSettings.showCalendarEventsPeriodTip) {#}
  1076. {#                monthRangeSliderTipToast = showToast(toastMsg, TOAST_TYPE.INFO, true, 60000 * 3);#}
  1077. {#            }#}
  1078. {#        });#}
  1079. {#    calendarEventsSelectorComponent.eventDispatcher.on(#}
  1080. {#        calendarEventsSelectorComponent.EVENTS.ON_DROPDOWN_HIDE, () => {#}
  1081. {#            if (monthRangeSliderTipToast) {#}
  1082. {#                monthRangeSliderTipToast.hideToast();#}
  1083. {#            }#}
  1084. {#        }#}
  1085. {#    );#}
  1086. {#});#}