Files
LMS/web/src/main/resources/static/js/tablekit.js
Vladislav Kopnin e22443b08e feat: submit all modal forms without page reload
- add a generic AJAX submitter for forms inside dialogs
- keep modal values and show field errors on server validation failure
- soft-refresh the page content on success, preserving scroll, search and filters
- share toast and re-init hooks for tables, markdown, sections and group page
- require passwords and add minlength/match validation on auth forms
2026-10-23 01:22:05 +03:00

207 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 == 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();
}
})();