Table columns: dropdown/text filters per column
- tablekit builds a filter row under the headers for every th with data-filter; distinct cell values become a select (label from data-filter-label), or a text substring filter with data-filter="text" - filters combine with the global table search; placeholder rows (colspan) are not filtered away; filter row follows the lab-focus column hiding - enabled on: matrix lab columns (by variant), defense table (ЛР, status with readable values), students (Основа, Пароль), lab assignment lists (Уровень, Источник, Сдача), labs list (Статус)
This commit is contained in:
@@ -1,33 +1,48 @@
|
||||
/* tablekit: сортировка по клику на заголовок + живой поиск по строке.
|
||||
Таблицу размечают: <table class="tablekit" id="...">, поле поиска
|
||||
<input class="table-search" data-target="id-таблицы">.
|
||||
Начальная сортировка (ФИО) выполняется на сервере; клик по заголовку меняет её. */
|
||||
/* tablekit: сортировка по клику на заголовок, поиск по таблице и фильтры по колонкам.
|
||||
Разметка:
|
||||
<table class="tablekit" id="t"> — таблица;
|
||||
<input class="table-search" data-target="t"> — поиск по строке;
|
||||
<th data-filter> — включить фильтр по колонке (выпадающий список значений);
|
||||
<th data-filter="text"> — текстовый фильтр по колонке;
|
||||
<th data-nosort> — колонка не сортируется;
|
||||
data-sort / data-filter-value / data-filter-label на <td> — значения и подписи.
|
||||
Начальная сортировка выполняется на сервере; клик по заголовку меняет её. */
|
||||
(function () {
|
||||
function normalize(value) {
|
||||
return (value || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
||||
return (value == null ? '' : String(value)).trim().toLowerCase().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function cellValue(cell) {
|
||||
function source(cell) {
|
||||
if (!cell) return '';
|
||||
if (cell.dataset && cell.dataset.sort !== undefined) return normalize(cell.dataset.sort);
|
||||
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 na = a.match(/-?\d+([.,]\d+)?/g);
|
||||
var nb = b.match(/-?\d+([.,]\d+)?/g);
|
||||
var fa = na ? parseFloat(na[0].replace(',', '.')) : NaN;
|
||||
var fb = nb ? parseFloat(nb[0].replace(',', '.')) : NaN;
|
||||
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 clearMarkers(table, exceptColumn) {
|
||||
var cells = table.tHead.rows[table.tHead.rows.length - 1].cells;
|
||||
for (var i = 0; i < cells.length; i++) {
|
||||
if (i === exceptColumn) continue;
|
||||
cells[i].classList.remove('is-asc', 'is-desc');
|
||||
}
|
||||
function headerRow(table) {
|
||||
return table.__headerRow || table.tHead.rows[table.tHead.rows.length - 1];
|
||||
}
|
||||
|
||||
function clearMarkers(table, keep) {
|
||||
Array.prototype.forEach.call(headerRow(table).cells, function (th, i) {
|
||||
if (i !== keep) th.classList.remove('is-asc', 'is-desc');
|
||||
});
|
||||
}
|
||||
|
||||
function sortColumn(table, index, direction) {
|
||||
@@ -35,22 +50,114 @@
|
||||
if (!body) return;
|
||||
var rows = Array.prototype.slice.call(body.rows);
|
||||
rows.sort(function (a, b) {
|
||||
return compared(cellValue(a.cells[index]), cellValue(b.cells[index]));
|
||||
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 cells = head.rows[head.rows.length - 1].cells;
|
||||
Array.prototype.forEach.call(cells, function (th, index) {
|
||||
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');
|
||||
th.title = th.title || 'Сортировать по этому столбцу';
|
||||
if (!th.title) th.title = 'Сортировать по этому столбцу';
|
||||
th.addEventListener('click', function (event) {
|
||||
if (event.target.closest('form, button, a, select, input')) return;
|
||||
var asc = !th.classList.contains('is-asc');
|
||||
@@ -60,16 +167,15 @@
|
||||
th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию');
|
||||
});
|
||||
});
|
||||
buildFilters(table);
|
||||
}
|
||||
|
||||
function bindSearch(input) {
|
||||
var table = document.getElementById(input.dataset.target);
|
||||
if (!table || !table.tBodies[0]) return;
|
||||
input.addEventListener('input', function () {
|
||||
var query = normalize(input.value);
|
||||
Array.prototype.forEach.call(table.tBodies[0].rows, function (row) {
|
||||
row.classList.toggle('row-hidden', !!query && normalize(row.textContent).indexOf(query) < 0);
|
||||
});
|
||||
table.__query = normalize(input.value);
|
||||
applyFilters(table);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user