From 2a0e8ddf9f3188fdb40a397c47e61453f93d2c4d Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Tue, 15 Sep 2026 22:43:48 +0300 Subject: [PATCH] Table columns: dropdown/text filters per column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 (Статус) --- web/src/main/resources/static/css/app.css | 7 + .../main/resources/static/js/admin-group.js | 2 +- web/src/main/resources/static/js/tablekit.js | 156 +++++++++++++++--- .../main/resources/templates/admin/group.html | 11 +- .../main/resources/templates/admin/lab.html | 6 +- .../main/resources/templates/admin/labs.html | 4 +- 6 files changed, 150 insertions(+), 36 deletions(-) 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: сортировка по клику на заголовок + живой поиск по строке. - Таблицу размечают: , поле поиска - . - Начальная сортировка (ФИО) выполняется на сервере; клик по заголовку меняет её. */ +/* 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 @@
- + -
СтудентЛРЗаданиеСтатусДействия
СтудентЛРЗаданиеСтатусДействия
@@ -130,7 +131,7 @@ — — + ждёт кода код выдан пишет ( c) @@ -173,7 +174,7 @@
- + diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html index bc91466..7ab840b 100644 --- a/web/src/main/resources/templates/admin/lab.html +++ b/web/src/main/resources/templates/admin/lab.html @@ -81,8 +81,8 @@
№ФИОЛогинОсноваВариантыПарольДействия
№ФИОЛогинОсноваВариантыПарольДействия
1
- - + + @@ -125,7 +125,7 @@ авто -
ВыборСтудентУровеньВариантИсточникСдачаВыборСтудентУровеньВариантИсточникСдача
+ попытка 1 ↓ diff --git a/web/src/main/resources/templates/admin/labs.html b/web/src/main/resources/templates/admin/labs.html index 58c8ce4..dcc3490 100644 --- a/web/src/main/resources/templates/admin/labs.html +++ b/web/src/main/resources/templates/admin/labs.html @@ -19,13 +19,13 @@
- + -
№НазваниеВариантовСтатус
№НазваниеВариантовСтатус
1 — 0 +