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