templates/js/library/data_table/custom_data_table.js line 1

Open in your IDE?
  1. class CustomDataTable {
  2.     options = {};
  3.     selector = null;
  4.     table = null;
  5.     eventDispatcher = null;
  6.     columnIds = {};
  7.     static EVENTS = {
  8.         ON_TABLE_FILTERED: 'tableFiltered'
  9.     };
  10.     constructor(selector, options = {}) {
  11.         this.selector = selector;
  12.         this.options = {
  13.             dom: 'i lfrtip',   // i = info сверху
  14.             "lengthMenu": [[10, 20, 30, 50, 100, 200, 300, 500, 700, -1], [10, 20, 30, 50, 100, 200, 300, 500, 700, "Все"]],
  15.             language: {
  16.                 url: '/js/vendor/datatables/ru.json'
  17.             },
  18.             drawRowNumbersForColIndex: undefined,
  19.             searchPrehandler: null, // Function to preprocess search terms before applying search
  20.             ...options
  21.         };
  22.         this.eventDispatcher = new EventDispatcher();
  23.         let $head = $(this.selector).find('thead');
  24.         let i = -1;
  25.         const me = this;
  26.         $head.find('th').each(function (){
  27.             i++;
  28.             let colId = $(this).data('table-col-id');
  29.             me.columnIds[i] = colId;
  30.         });
  31.     }
  32.     static getOptionsExample() {
  33.         return {
  34.             "lengthMenu": [[10, 20, 50, 100, 200, 300, 500, 700, -1], [10, 20, 50, 100, 200, 300, 500, 700, "Все"]],
  35.             "pageLength": 10, // Number of rows to display by default
  36.             "columnDefs": [ // Columns definitions
  37.                 {
  38.                     "targets": 0,
  39.                     "type": "num",
  40.                     "visible": false,
  41.                 },
  42.                 {
  43.                     "targets": 1,
  44.                     "type": "date-d-m-Y"
  45.                 },
  46.             ],
  47.             drawRowNumbersForColIndex: 0,
  48.             "searchPrehandler": (searchTerm) => {
  49.                 // Example: if user enters "111", search for both "111" and "222"
  50.                 if (searchTerm === '111') {
  51.                     return '111|222';
  52.                 }
  53.                 return searchTerm;
  54.             },
  55.             "dropdownFiltersConfig": {
  56.                 "elementsData": [
  57.                     {
  58.                         "columnIndex": 2, //3th column
  59.                         "dropdownId": "departmentDropdown", // Dropdown button ID
  60.                         "badgeId": "departmentBadge", // Badge ID to show filter status (optional)
  61.                         "textForAll": "All" // Text to show when 'all' is selected (optional)
  62.                     },
  63.                 ]
  64.             },
  65.         }
  66.     }
  67.     getColumnIndexByColumnId(colId) {
  68.         for (let index in this.columnIds) {
  69.             if (this.columnIds[index] === colId) {
  70.                 return parseInt(index);
  71.             }
  72.         }
  73.         return -1;
  74.     }
  75.     getAllFilteredTableData() {
  76.         let allData = [];
  77.         this.table.rows({ search: 'applied' }).every((rowIdx) => {
  78.             let row = this.table.row(rowIdx);
  79.             allData.push(row.data());
  80.         });
  81.         return allData;
  82.     }
  83.     getFilteredData() {
  84.         return this.table.rows({ search: 'applied', order: 'applied' }).data().toArray();
  85.     }
  86.     getHeadElement() {
  87.         return $(this.table.table().header());
  88.     }
  89.     downloadCsv(filename = 'data_table_export.csv') {
  90.         const delimiter = ';';
  91.         const rows = [];
  92.         // collect headers and escape
  93.         const headers = [];
  94.         this.table.columns().every((index) => {
  95.             const $header = $(this.table.column(index).header());
  96.             if ($header.is('[data-export-exclude-col]')) return;
  97.             const raw = $header.text();
  98.             const safe = String(raw).replace(/"/g, '""');
  99.             headers.push(`"${safe}"`);
  100.         });
  101.         rows.push(headers.join(delimiter));
  102.         // collect rows (respect search: 'applied')
  103.         let number = 0;
  104.         this.table.rows({ search: 'applied' }).every((rowIdx) => {
  105.             const row = this.table.row(rowIdx);
  106.             const rowData = [];
  107.             row.columns().every((colIdx) => {
  108.                 const $header = $(this.table.column(colIdx).header());
  109.                 if ($header.is('[data-export-exclude-col]')) return;
  110.                 let cell = row.data()[colIdx];
  111.                 if (this.options.drawRowNumbersForColIndex === colIdx) {
  112.                     cell = ++number;
  113.                 }
  114.                 const raw = cell === null || cell === undefined ? '' : String(cell);
  115.                 const safe = raw.replace(/"/g, '""');
  116.                 rowData.push(`"${safe}"`);
  117.             });
  118.             rows.push(rowData.join(delimiter));
  119.         });
  120.         // Join with CRLF for Windows compatibility and prefix with UTF-8 BOM
  121.         const csvString = '\uFEFF' + rows.join('\r\n');
  122.         const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
  123.         const link = document.createElement('a');
  124.         if (link.download !== undefined) {
  125.             const url = URL.createObjectURL(blob);
  126.             link.setAttribute('href', url);
  127.             link.setAttribute('download', filename);
  128.             link.style.visibility = 'hidden';
  129.             document.body.appendChild(link);
  130.             link.click();
  131.             document.body.removeChild(link);
  132.         }
  133.     }
  134.     initDataTable() {
  135.         let columnDefs = [];
  136.         let dropdownFiltersConfig = {
  137.             elementsData: []
  138.         };
  139.         const me = this;
  140.         $(`${this.selector} th`).each(function(index) {
  141.             let hideCol = $(this).is('[data-table-hide-col]');
  142.             let tableType = $(this).data('table-type');
  143.             let sortOff = $(this).is('[data-table-sort-off]');
  144.             if ($(this).is('[data-table-default-sort-col]') && (!me.options.order || me.options.order.length === 0) && !sortOff) {
  145.                 me.options.order = [[index, 'asc']];
  146.             }
  147.             let optionsItem = {
  148.                 "targets": index,
  149.             };
  150.             if (hideCol) {
  151.                 optionsItem['visible'] = false;
  152.             }
  153.             if (tableType) {
  154.                 optionsItem['type'] = tableType;
  155.             }
  156.             if (sortOff) {
  157.                 optionsItem['orderable'] = false;
  158.             }
  159.             if (Object.keys(optionsItem).length > 1) {
  160.                 columnDefs.push(optionsItem);
  161.             }
  162.             let dropdownFilterConfig_elementsDataItem = {};
  163.             let dropdownId = $(this).data('table-dropdown-id');
  164.             if (dropdownId) {
  165.                 dropdownFilterConfig_elementsDataItem['columnIndex'] = index;
  166.                 dropdownFilterConfig_elementsDataItem['dropdownId'] = dropdownId;
  167.                 let badgeId = $(this).data('table-badge-id');
  168.                 if (badgeId) {
  169.                     dropdownFilterConfig_elementsDataItem['badgeId'] = badgeId;
  170.                 }
  171.                 let textForAll = $(this).data('table-dropdown-text-for-all');
  172.                 if (textForAll) {
  173.                     dropdownFilterConfig_elementsDataItem['textForAll'] = textForAll;
  174.                 }
  175.                 dropdownFiltersConfig.elementsData.push(dropdownFilterConfig_elementsDataItem);
  176.             }
  177.             let uniqueValues = $(this).data('table-dropdown-values');
  178.             if (uniqueValues) {
  179.                 uniqueValues = uniqueValues.split('|').map(v => v.trim());
  180.                 dropdownFilterConfig_elementsDataItem['uniqueValues'] = uniqueValues;
  181.             }
  182.         });
  183.         if (!this.options.columnDefs || this.options.columnDefs.length === 0) {
  184.             console.log(columnDefs)
  185.             this.options.columnDefs = columnDefs;
  186.         }
  187.         if (dropdownFiltersConfig && (!this.options.dropdownFiltersConfig
  188.             || !this.options.dropdownFiltersConfig.elementsData
  189.             || this.options.dropdownFiltersConfig.elementsData.length === 0)) {
  190.             this.options.dropdownFiltersConfig = dropdownFiltersConfig;
  191.         }
  192.         const originalInitComplete = this.options.initComplete;
  193.         this.options.initComplete = function(settings, json) {
  194.             if (originalInitComplete) {
  195.                 originalInitComplete.call(this, settings, json);
  196.             }
  197.             me.overrideSearchEngine(settings);
  198.         };
  199.         this.table = $(this.selector).DataTable(this.options);
  200.         $(document).ready(() => {
  201.             this.populateDropdowns();
  202.             this.table.on('draw', () => {
  203.                 this.populateDropdowns();
  204.             });
  205.         });
  206.         this.eventDispatcher.addEventListener(CustomDataTable.EVENTS.ON_TABLE_FILTERED, () => {
  207.             if (this.options.drawRowNumbersForColIndex !== null && this.options.drawRowNumbersForColIndex !== undefined) {
  208.                 this.drawRowNumbers();
  209.             }
  210.         });
  211.         return this.table;
  212.     }
  213.     overrideSearchEngine(settings) {
  214.         const me = this;
  215.         const api = new $.fn.dataTable.Api(settings);
  216.         const searchInput = $(api.table().container()).find('.dataTables_filter input');
  217.         if (searchInput.length === 0) {
  218.             return;
  219.         }
  220.         const newInput = searchInput.clone();
  221.         newInput.val(searchInput.val());
  222.         searchInput.replaceWith(newInput);
  223.         newInput.on('keyup search input', function() {
  224.             let preprocessedSearchValue = this.value;
  225.             if (StringUtils.containsEngString(preprocessedSearchValue)) {
  226.                 let converted = StringUtils.engKeyboardLayoutToRus(preprocessedSearchValue);
  227.                 converted = StringUtils.escapeRegex(converted);
  228.                 preprocessedSearchValue = preprocessedSearchValue +
  229.                     "|" + converted;
  230.             }
  231.             if (me.options.searchPrehandler && typeof me.options.searchPrehandler === 'function') {
  232.                 preprocessedSearchValue = me.options.searchPrehandler(preprocessedSearchValue);
  233.             }
  234.             api.search(preprocessedSearchValue, true, false).draw();
  235.         });
  236.     }
  237.     drawRowNumbers() {
  238.         this.table.column(this.options.drawRowNumbersForColIndex, { search: 'applied' }).nodes().each(function(cell, i) {
  239.             cell.innerHTML = i + 1;
  240.         });
  241.     }
  242.     populateDropdowns() {
  243.         if (this.options.dropdownFiltersConfig && this.options.dropdownFiltersConfig.elementsData) {
  244.             this.options.dropdownFiltersConfig.elementsData.forEach((options) => {
  245.                 this.populateDropdown(options);
  246.             });
  247.         }
  248.     }
  249.     populateDropdown(options) {
  250.         const me = this;
  251.         options = {
  252.             columnIndex: null,
  253.             dropdownId: null,
  254.             badgeId: null,
  255.             textForAll: 'Все',
  256.             uniqueValues: null,
  257.             ...options
  258.         };
  259.         var dropdownButton = $("#" + options.dropdownId);
  260.         var dropdownMenu = dropdownButton.next('.dropdown-menu');
  261.         // Read multiple flag from data attribute on button (template sets data-multiple)
  262.         var isMultiple = false;
  263.         try {
  264.             var dataMultiple = dropdownButton.data('multiple');
  265.             if (typeof dataMultiple === 'boolean') {
  266.                 isMultiple = dataMultiple;
  267.             } else if (typeof dataMultiple === 'string') {
  268.                 isMultiple = dataMultiple === 'true';
  269.             }
  270.         } catch (e) {
  271.             isMultiple = false;
  272.         }
  273.         // Get unique values from column
  274.         if (!options.uniqueValues) {
  275.             options.uniqueValues = this.table
  276.                 .column(options.columnIndex)
  277.                 .data()
  278.                 .unique()
  279.                 .sort()
  280.                 .toArray();
  281.         }
  282.         // Remove previously generated non-all items
  283.         dropdownMenu.find('.dropdown-item:not([data-filter="all"])').remove();
  284.         // Append new items (галочка вместо checkbox)
  285.         options.uniqueValues.forEach(function(value) {
  286.             var item = $('<a class="dropdown-item d-flex align-items-center" href="#" role="menuitem" aria-pressed="false"></a>');
  287.             item.attr('data-filter', value);
  288.             // Общий контейнер с галочкой (скрыт до выбора)
  289.             var checkWrap = $('<span class="filter-check me-2" style="visibility:hidden;"></span>');
  290.             var checkIcon = $('<i class="fas fa-check text-white"></i>');
  291.             checkWrap.append(checkIcon);
  292.             var textSpan = $('<span class="filter-text"></span>').text(value);
  293.             item.append(checkWrap).append(textSpan);
  294.             dropdownMenu.append(item);
  295.         });
  296.         // Ensure no duplicate handlers
  297.         dropdownMenu.find('.dropdown-item').off('click');
  298.         var selectedValues = dropdownButton.data('selectedValues') || [];
  299.         function updateCheckMark($el, selected) {
  300.             var wrap = $el.find('.filter-check');
  301.             if (!wrap.length) return;
  302.             if (selected) {
  303.                 wrap.css('visibility', 'visible');
  304.                 $el.addClass('active').attr('aria-pressed', 'true');
  305.             } else {
  306.                 wrap.css('visibility', 'hidden');
  307.                 $el.removeClass('active').attr('aria-pressed', 'false');
  308.             }
  309.         }
  310.         // Initialize existing selections
  311.         if (selectedValues && selectedValues.length > 0) {
  312.             dropdownMenu.find('.dropdown-item:not([data-filter="all"])').each(function() {
  313.                 var $it = $(this);
  314.                 var val = $it.data('filter');
  315.                 updateCheckMark($it, selectedValues.indexOf(val) !== -1);
  316.             });
  317.             if (selectedValues.length === 1) {
  318.                 dropdownButton.text(selectedValues[0]);
  319.                 if (options.badgeId) $(options.badgeId).removeClass('d-none').text('Filter: ' + selectedValues[0]);
  320.             } else {
  321.                 dropdownButton.text(selectedValues.length + ' выбрано');
  322.                 if (options.badgeId) $(options.badgeId).removeClass('d-none').text('Filter: ' + selectedValues.length);
  323.             }
  324.         } else {
  325.             dropdownButton.text(options.textForAll || 'Все');
  326.             if (options.badgeId) $(options.badgeId).addClass('d-none');
  327.         }
  328.         dropdownMenu.find('.dropdown-item').on('click', function(e) {
  329.             e.preventDefault();
  330.             var $el = $(this);
  331.             var filterValue = $el.data('filter');
  332.             if (!isMultiple) {
  333.                 if (filterValue === 'all') {
  334.                     dropdownButton.data('selectedValues', []);
  335.                     dropdownMenu.find('.dropdown-item:not([data-filter="all"])').each(function(){ updateCheckMark($(this), false); });
  336.                     dropdownButton.text(options.textForAll || 'Все');
  337.                     if (options.badgeId) $(options.badgeId).addClass('d-none');
  338.                     me.table.column(options.columnIndex).search('').draw();
  339.                 } else {
  340.                     dropdownButton.data('selectedValues', [filterValue]);
  341.                     dropdownMenu.find('.dropdown-item:not([data-filter="all"])').each(function(){
  342.                         var $other = $(this);
  343.                         updateCheckMark($other, $other.data('filter') === filterValue);
  344.                     });
  345.                     dropdownButton.text(filterValue);
  346.                     if (options.badgeId) $(options.badgeId).removeClass('d-none').text('Filter: ' + filterValue);
  347.                     let esc = StringUtils.escapeRegex(filterValue);
  348.                     me.table.column(options.columnIndex).search('^' + esc + '$', true, false).draw();
  349.                 }
  350.             } else {
  351.                 if (filterValue === 'all') {
  352.                     selectedValues = [];
  353.                     dropdownButton.data('selectedValues', selectedValues);
  354.                     dropdownMenu.find('.dropdown-item:not([data-filter="all"])').each(function(){ updateCheckMark($(this), false); });
  355.                     dropdownButton.text(options.textForAll || 'Все');
  356.                     if (options.badgeId) $(options.badgeId).addClass('d-none');
  357.                     me.table.column(options.columnIndex).search('').draw();
  358.                 } else {
  359.                     var idx = selectedValues.indexOf(filterValue);
  360.                     if (idx === -1) {
  361.                         selectedValues.push(filterValue);
  362.                         updateCheckMark($el, true);
  363.                     } else {
  364.                         selectedValues.splice(idx, 1);
  365.                         updateCheckMark($el, false);
  366.                     }
  367.                     if (selectedValues.length === 0) {
  368.                         dropdownButton.text(options.textForAll || 'Все');
  369.                         if (options.badgeId) $(options.badgeId).addClass('d-none');
  370.                         dropdownButton.data('selectedValues', selectedValues);
  371.                         me.table.column(options.columnIndex).search('').draw();
  372.                     } else {
  373.                         if (selectedValues.length === 1) {
  374.                             dropdownButton.text(selectedValues[0]);
  375.                             if (options.badgeId) $(options.badgeId).removeClass('d-none').text('Filter: ' + selectedValues[0]);
  376.                         } else {
  377.                             dropdownButton.text(selectedValues.length + ' выбрано');
  378.                             if (options.badgeId) $(options.badgeId).removeClass('d-none').text('Filter: ' + selectedValues.length);
  379.                         }
  380.                         var escaped = selectedValues.map(function(v){ return StringUtils.escapeRegex(v); });
  381.                         var regex = '^(' + escaped.join('|') + ')$';
  382.                         dropdownButton.data('selectedValues', selectedValues);
  383.                         me.table.column(options.columnIndex).search(regex, true, false).draw();
  384.                     }
  385.                 }
  386.             }
  387.             me.eventDispatcher.emit(CustomDataTable.EVENTS.ON_TABLE_FILTERED);
  388.         });
  389.     }
  390. }