/* tablekit: сортировка по клику на заголовок, поиск по таблице и фильтры по колонкам. Разметка:
| — включить фильтр по колонке (выпадающий список значений); | — текстовый фильтр по колонке; | — колонка не сортируется; data-sort / data-filter-value / data-filter-label на | — значения и подписи. Начальная сортировка выполняется на сервере; клик по заголовку меняет её. */ (function () { function normalize(value) { return (value == null ? '' : String(value)).trim().toLowerCase().replace(/\s+/g, ' '); } function source(cell) { if (!cell) return ''; if (cell.dataset.filterValue !== undefined) return normalize(cell.dataset.filterValue); if (cell.dataset.sort !== undefined) return normalize(cell.dataset.sort); return normalize(cell.textContent); } function label(cell) { if (!cell) return ''; if (cell.dataset.filterLabel !== undefined) return cell.dataset.filterLabel.trim(); var text = (cell.textContent || '').trim().replace(/\s+/g, ' '); return text || source(cell); } function compared(a, b) { var ma = (a.match(/-?\d+([.,]\d+)?/) || [''])[0].replace(',', '.'); var mb = (b.match(/-?\d+([.,]\d+)?/) || [''])[0].replace(',', '.'); var fa = parseFloat(ma); var fb = parseFloat(mb); if (!isNaN(fa) && !isNaN(fb) && fa !== fb) return fa - fb; return a.localeCompare(b, 'ru', { numeric: true, sensitivity: 'base' }); } function headerRow(table) { return table.__headerRow || table.tHead.rows[table.tHead.rows.length - 1]; } function clearAllMarkers(table) { Array.prototype.forEach.call(headerRow(table).cells, function (th) { th.classList.remove('is-asc', 'is-desc'); }); } function sortColumn(table, index, direction) { var body = table.tBodies[0]; if (!body) return; var rows = Array.prototype.slice.call(body.rows); rows.sort(function (a, b) { return compared(source(a.cells[index]), source(b.cells[index])); }); if (direction < 0) rows.reverse(); rows.forEach(function (row) { body.appendChild(row); }); } function applyFilters(table) { var body = table.tBodies[0]; if (!body) return; var query = table.__query || ''; var filters = table.__filters || {}; var columnCount = headerRow(table).cells.length; Array.prototype.forEach.call(body.rows, function (row) { var placeholder = row.cells.length < columnCount; var show = !query || normalize(row.textContent).indexOf(query) >= 0; if (show && !placeholder) { for (var key in filters) { var filter = filters[key]; if (!filter || !filter.value) continue; var value = source(row.cells[key]); var match = filter.kind === 'text' ? value.indexOf(filter.value) >= 0 : value === filter.value; if (!match) { show = false; break; } } } row.classList.toggle('row-hidden', !show); }); } function buildFilters(table) { var head = table.tHead; var body = table.tBodies[0]; if (!head || !body) return; var row = headerRow(table); var columns = []; Array.prototype.forEach.call(row.cells, function (th, index) { if (th.hasAttribute('data-filter')) { th.classList.add('th-filterable'); columns.push(index); } }); if (!columns.length) return; var filterRow = document.createElement('tr'); filterRow.className = 'filter-row'; Array.prototype.forEach.call(row.cells, function (th, index) { var cell = document.createElement('th'); if (th.dataset.lab) cell.setAttribute('data-lab', th.dataset.lab); if (columns.indexOf(index) >= 0) { var kind = th.getAttribute('data-filter') || 'values'; var control; if (kind === 'text') { control = document.createElement('input'); control.type = 'text'; control.placeholder = 'фильтр…'; } else { control = document.createElement('select'); var any = document.createElement('option'); any.value = ''; any.textContent = 'все'; control.appendChild(any); var seen = {}; var options = []; Array.prototype.forEach.call(body.rows, function (tr) { var td = tr.cells[index]; if (!td) return; var value = source(td); if (!value || seen[value]) return; seen[value] = 1; options.push({ value: value, label: label(td) }); }); options.sort(function (a, b) { return compared(a.label, b.label); }); options.forEach(function (option) { var item = document.createElement('option'); item.value = option.value; item.textContent = option.label; control.appendChild(item); }); } control.className = 'filter-control'; control.setAttribute('aria-label', (th.textContent || '').trim() + ' — фильтр'); control.addEventListener(kind === 'text' ? 'input' : 'change', function () { table.__filters = table.__filters || {}; table.__filters[index] = { kind: kind, value: kind === 'text' ? normalize(control.value) : control.value, }; applyFilters(table); }); cell.appendChild(control); } filterRow.appendChild(cell); }); head.appendChild(filterRow); } function bind(table) { if (table.dataset.tablekitBound) return; table.dataset.tablekitBound = '1'; var head = table.tHead; if (!head) return; var row = head.rows[head.rows.length - 1]; table.__headerRow = row; Array.prototype.forEach.call(row.cells, function (th, index) { if (th.hasAttribute('data-nosort')) return; th.classList.add('th-sortable'); if (!th.title) th.title = 'Сортировать по этому столбцу'; // сортировка с клавиатуры: фокус + Enter/пробел, состояние доступно скринридеру th.tabIndex = 0; if (th.classList.contains('is-asc')) th.setAttribute('aria-sort', 'ascending'); function doSort() { var asc = !th.classList.contains('is-asc'); clearAllMarkers(table); th.classList.add(asc ? 'is-asc' : 'is-desc'); th.setAttribute('aria-sort', asc ? 'ascending' : 'descending'); sortColumn(table, index, asc ? 1 : -1); th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию'); } th.addEventListener('click', function (event) { if (event.target.closest('form, button, a, select, input')) return; doSort(); }); th.addEventListener('keydown', function (event) { if (event.target !== th) return; if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); doSort(); }); }); buildFilters(table); } function bindSearch(input) { var table = document.getElementById(input.dataset.target); if (!table || !table.tBodies[0]) return; input.addEventListener('input', function () { table.__query = normalize(input.value); applyFilters(table); }); } function init() { document.querySelectorAll('table.tablekit').forEach(bind); document.querySelectorAll('input.table-search').forEach(bindSearch); } window.__lmsInitTablekit = init; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); |
|---|