Tables: initial ФИО order, click-to-sort columns, substring search

- identity group detail (and thus every student list) is ordered by full name;
  defense rows on the group page sorted by name + lab number
- tablekit.js: click a header to sort (numeric-aware, ru locale), second click
  flips direction (▲/▼ indicator); search input with data-target filters rows
  by substring live
- tables wired: group matrix (with data-sort on variant cells), defense table
  (data-sort on status/date), students table, lab assignment lists per group,
  groups list and labs list; name/number columns pre-marked ascending
- non-sortable columns marked data-nosort (checkbox/actions)
This commit is contained in:
2026-09-15 22:38:42 +03:00
parent d04207302a
commit b212eed546
9 changed files with 128 additions and 32 deletions

View File

@@ -329,3 +329,12 @@ details.sub-acc[open] > summary .sub-arrow { transform: rotate(-135deg); }
.lab-head-row { display: flex; align-items: center; justify-content: center; gap: 0.3rem; }
.actions-cell { white-space: nowrap; }
.actions-cell .inline-form { display: inline-block; }
/* tablekit: сортируемые таблицы и поиск */
th.th-sortable { cursor: pointer; user-select: none; }
th.th-sortable:hover { background: var(--c-surface); }
th.th-sortable.is-asc::after { content: " ▲"; color: var(--c-text-muted); font-size: 0.7rem; }
th.th-sortable.is-desc::after { content: " ▼"; color: var(--c-text-muted); font-size: 0.7rem; }
.row-hidden { display: none !important; }
.table-search { width: 14rem; }
@media (max-width: 640px) { .table-search { width: 100%; } }

View File

@@ -0,0 +1,80 @@
/* tablekit: сортировка по клику на заголовок + живой поиск по строке.
Таблицу размечают: <table class="tablekit" id="...">, поле поиска
<input class="table-search" data-target="id-таблицы">.
Начальная сортировка (ФИО) выполняется на сервере; клик по заголовку меняет её. */
(function () {
function normalize(value) {
return (value || '').trim().toLowerCase().replace(/\s+/g, ' ');
}
function cellValue(cell) {
if (!cell) return '';
if (cell.dataset && cell.dataset.sort !== undefined) return normalize(cell.dataset.sort);
return normalize(cell.textContent);
}
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;
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 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(cellValue(a.cells[index]), cellValue(b.cells[index]));
});
if (direction < 0) rows.reverse();
rows.forEach(function (row) { body.appendChild(row); });
}
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) {
if (th.hasAttribute('data-nosort')) return;
th.classList.add('th-sortable');
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');
clearMarkers(table, index);
th.classList.add(asc ? 'is-asc' : 'is-desc');
sortColumn(table, index, asc ? 1 : -1);
th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию');
});
});
}
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);
});
});
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('table.tablekit').forEach(bind);
document.querySelectorAll('input.table-search').forEach(bindSearch);
});
})();