diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index de86a24..392b0e5 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -338,3 +338,10 @@ th.th-sortable.is-desc::after { content: " ▼"; color: var(--c-text-muted); fon .row-hidden { display: none !important; } .table-search { width: 14rem; } @media (max-width: 640px) { .table-search { width: 100%; } } +.filter-row th { padding: 0.25rem 0.35rem; background: var(--c-bg); } +.filter-control { + width: 100%; min-width: 3.5rem; font-size: 0.72rem; line-height: 1.4; + padding: 0.1rem 0.25rem; border: 1px solid var(--c-border); border-radius: 0.35rem; + background: var(--c-surface); color: var(--c-text); font-family: inherit; +} +th.th-filterable { padding-bottom: 0.35rem; } diff --git a/web/src/main/resources/static/js/admin-group.js b/web/src/main/resources/static/js/admin-group.js index 48064bd..6354b16 100644 --- a/web/src/main/resources/static/js/admin-group.js +++ b/web/src/main/resources/static/js/admin-group.js @@ -104,7 +104,7 @@ function focusLab() { var s = $id('lab-focus'); if (!s) return; var labId = s.value; - document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab]').forEach(function (el) { + document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab], .filter-row th[data-lab]').forEach(function (el) { el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none'; }); } diff --git a/web/src/main/resources/static/js/tablekit.js b/web/src/main/resources/static/js/tablekit.js index 4f8241d..346d095 100644 --- a/web/src/main/resources/static/js/tablekit.js +++ b/web/src/main/resources/static/js/tablekit.js @@ -1,33 +1,48 @@ -/* tablekit: сортировка по клику на заголовок + живой поиск по строке. - Таблицу размечают:
| — включить фильтр по колонке (выпадающий список значений); + | — текстовый фильтр по колонке; + | — колонка не сортируется; + data-sort / data-filter-value / data-filter-label на | — значения и подписи.
+ Начальная сортировка выполняется на сервере; клик по заголовку меняет её. */
(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);
});
}
diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html
index 5f9e655..c1281c7 100644
--- a/web/src/main/resources/templates/admin/group.html
+++ b/web/src/main/resources/templates/admin/group.html
@@ -50,7 +50,7 @@
| Студент |
-
+ |
|
ЛР
@@ -75,7 +75,8 @@
—
—
-
+ |
|
@@ -121,7 +122,7 @@
|
|---|