v1.8.6: сортировка таблиц, спиннеры number input, единый стиль — fix broken HTML, modal-actions → form-actions, card-wrapper
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* sort-table.js — Универсальный модуль сортировки таблиц.
|
||||
*
|
||||
* Автоматически находит все <table> на странице и делает
|
||||
* заголовки <th> кликабельными для сортировки по столбцам.
|
||||
*
|
||||
* Использование:
|
||||
* 1. Подключить <script src="/service/static/js/sort-table.js"></script>
|
||||
* 2. Таблицы автоматически получают сортировку при загрузке
|
||||
* 3. Для AJAX-таблиц: вызвать sortTables() после заполнения <tbody>
|
||||
*/
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
var SORT_CSS = [
|
||||
'.sort-th { cursor:pointer; user-select:none; position:relative; padding-right:18px !important; }',
|
||||
'.sort-th:hover { color:var(--accent); }',
|
||||
'.sort-th::after { content:""; position:absolute; right:6px; top:50%; transform:translateY(-50%); width:0; height:0; border-left:4px solid transparent; border-right:4px solid transparent; border-top:5px solid var(--text-muted); opacity:0.4; }',
|
||||
'.sort-th.sort-asc::after { border-top:none; border-bottom:5px solid var(--accent); opacity:1; }',
|
||||
'.sort-th.sort-desc::after { border-top:5px solid var(--accent); opacity:1; }'
|
||||
].join('\n');
|
||||
|
||||
/* Встраиваем стили один раз */
|
||||
function injectStyles(){
|
||||
if(document.getElementById('sort-table-styles')) return;
|
||||
var style = document.createElement('style');
|
||||
style.id = 'sort-table-styles';
|
||||
style.textContent = SORT_CSS;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/* Определение типа значения ячейки */
|
||||
function getCellValue(tr, idx){
|
||||
var td = tr.children[idx];
|
||||
if(!td) return '';
|
||||
var text = (td.textContent || '').trim();
|
||||
return text;
|
||||
}
|
||||
|
||||
/* Парсинг числа из текста */
|
||||
function parseNumber(text){
|
||||
if(!text || text === '—' || text === '-') return NaN;
|
||||
/* Убираем пробелы,.currency-символы, % */
|
||||
var cleaned = text.replace(/[^\d,\.\-]/g, '').replace(',', '.');
|
||||
var num = parseFloat(cleaned);
|
||||
return isNaN(num) ? NaN : num;
|
||||
}
|
||||
|
||||
/* Парсинг даты из текста (DD.MM.YYYY или YYYY-MM-DD) */
|
||||
function parseDate(text){
|
||||
if(!text) return NaN;
|
||||
/* DD.MM.YYYY HH:MM */
|
||||
var m = text.match(/(\d{2})\.(\d{2})\.(\d{4})/);
|
||||
if(m) return new Date(m[3], m[2]-1, m[1]).getTime();
|
||||
/* YYYY-MM-DD */
|
||||
m = text.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||||
if(m) return new Date(m[1], m[2]-1, m[3]).getTime();
|
||||
return NaN;
|
||||
}
|
||||
|
||||
/* Сравнение двух значений */
|
||||
function compare(a, b, type){
|
||||
if(type === 'number'){
|
||||
var na = parseNumber(a), nb = parseNumber(b);
|
||||
if(isNaN(na) && isNaN(nb)) return 0;
|
||||
if(isNaN(na)) return 1;
|
||||
if(isNaN(nb)) return -1;
|
||||
return na - nb;
|
||||
}
|
||||
if(type === 'date'){
|
||||
var da = parseDate(a), db = parseDate(b);
|
||||
if(isNaN(da) && isNaN(db)) return 0;
|
||||
if(isNaN(da)) return 1;
|
||||
if(isNaN(db)) return -1;
|
||||
return da - db;
|
||||
}
|
||||
/* Текст */
|
||||
return a.localeCompare(b, 'ru');
|
||||
}
|
||||
|
||||
/* Определение типа столбца по первым строкам */
|
||||
function detectColumnType(tbody, idx){
|
||||
var rows = tbody.children;
|
||||
var samples = [];
|
||||
var limit = Math.min(rows.length, 10);
|
||||
for(var i = 0; i < limit; i++){
|
||||
samples.push(getCellValue(rows[i], idx));
|
||||
}
|
||||
var numCount = 0, dateCount = 0;
|
||||
for(var j = 0; j < samples.length; j++){
|
||||
if(parseNumber(samples[j]) !== NaN && samples[j] !== '') numCount++;
|
||||
if(parseDate(samples[j]) !== NaN && samples[j] !== '') dateCount++;
|
||||
}
|
||||
if(samples.length > 0 && numCount / samples.length > 0.6) return 'number';
|
||||
if(samples.length > 0 && dateCount / samples.length > 0.6) return 'date';
|
||||
return 'text';
|
||||
}
|
||||
|
||||
/* Привязка сортировки к таблице */
|
||||
function attachSort(table){
|
||||
if(table.dataset.sortAttached) return;
|
||||
table.dataset.sortAttached = '1';
|
||||
|
||||
var thead = table.querySelector('thead');
|
||||
var tbody = table.querySelector('tbody');
|
||||
if(!thead || !tbody) return;
|
||||
|
||||
var headers = thead.querySelectorAll('th');
|
||||
headers.forEach(function(th, idx){
|
||||
/* Пропускаем колонку действий */
|
||||
if(th.classList.contains('actions')) return;
|
||||
|
||||
th.classList.add('sort-th');
|
||||
th.dataset.sortIdx = idx;
|
||||
th.dataset.sortDir = 'none';
|
||||
|
||||
th.addEventListener('click', function(){
|
||||
var colIdx = parseInt(this.dataset.sortIdx);
|
||||
var currentDir = this.dataset.sortDir;
|
||||
var newDir = currentDir === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
/* Сброс других столбцов */
|
||||
headers.forEach(function(h){ h.classList.remove('sort-asc', 'sort-desc'); h.dataset.sortDir = 'none'; });
|
||||
this.classList.remove('sort-asc', 'sort-desc');
|
||||
this.classList.add('sort-' + newDir);
|
||||
this.dataset.sortDir = newDir;
|
||||
|
||||
/* Определение типа данных */
|
||||
var colType = detectColumnType(tbody, colIdx);
|
||||
|
||||
/* Сбор строк с значениями */
|
||||
var rowsArray = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
|
||||
rowsArray.sort(function(rowA, rowB){
|
||||
var valA = getCellValue(rowA, colIdx);
|
||||
var valB = getCellValue(rowB, colIdx);
|
||||
var result = compare(valA, valB, colType);
|
||||
return newDir === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
/* Перестановка строк */
|
||||
rowsArray.forEach(function(row){ tbody.appendChild(row); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* Публичная функция: инициализация/переинициализация всех таблиц */
|
||||
function sortTables(){
|
||||
injectStyles();
|
||||
var tables = document.querySelectorAll('table');
|
||||
for(var i = 0; i < tables.length; i++){
|
||||
attachSort(tables[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Автозапуск после загрузки DOM */
|
||||
if(document.readyState === 'loading'){
|
||||
document.addEventListener('DOMContentLoaded', sortTables);
|
||||
} else {
|
||||
sortTables();
|
||||
}
|
||||
|
||||
/* Экспорт в глобальную область */
|
||||
window.sortTables = sortTables;
|
||||
})();
|
||||
Reference in New Issue
Block a user