168 lines
5.4 KiB
JavaScript
168 lines
5.4 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define([], factory);
|
|
} else if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else {
|
|
root.USortableTable = factory();
|
|
}
|
|
}(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
|
|
|
|
function getTable(target) {
|
|
if (!target) return null;
|
|
if (typeof target === 'string') return document.getElementById(target);
|
|
return target.nodeType === 1 ? target : null;
|
|
}
|
|
|
|
function getStorageKey(table, options) {
|
|
if (options && options.storageKey) return options.storageKey;
|
|
if (table && table.id) return `u-sortable-table.${table.id}`;
|
|
return null;
|
|
}
|
|
|
|
function loadSort(table, options) {
|
|
const storageKey = getStorageKey(table, options);
|
|
if (!storageKey || !globalScope.localStorage) return null;
|
|
try {
|
|
const raw = globalScope.localStorage.getItem(storageKey);
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed || !Number.isInteger(parsed.column) || (parsed.direction !== 'asc' && parsed.direction !== 'desc')) {
|
|
return null;
|
|
}
|
|
return parsed;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function saveSort(table, column, direction, options) {
|
|
const storageKey = getStorageKey(table, options);
|
|
if (!storageKey || !globalScope.localStorage) return;
|
|
try {
|
|
globalScope.localStorage.setItem(storageKey, JSON.stringify({ column, direction }));
|
|
} catch (error) {
|
|
// Ignore storage errors.
|
|
}
|
|
}
|
|
|
|
function parseSortValue(text) {
|
|
const normalized = String(text || '').trim();
|
|
if (!normalized) return { type: 'text', value: '' };
|
|
|
|
if (globalScope.UFormat && typeof globalScope.UFormat.parseUnitNumber === 'function') {
|
|
const unitValue = globalScope.UFormat.parseUnitNumber(normalized);
|
|
if (unitValue != null) {
|
|
return { type: 'number', value: unitValue };
|
|
}
|
|
}
|
|
|
|
const timestamp = Date.parse(normalized);
|
|
if (!Number.isNaN(timestamp)) {
|
|
return { type: 'number', value: timestamp };
|
|
}
|
|
|
|
return { type: 'text', value: normalized.toLowerCase() };
|
|
}
|
|
|
|
function setHeaderState(table, columnIndex, direction) {
|
|
Array.from(table.querySelectorAll('thead th')).forEach((header, index) => {
|
|
header.classList.remove('sorted-asc', 'sorted-desc');
|
|
header.setAttribute('aria-sort', 'none');
|
|
if (index === columnIndex) {
|
|
header.classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');
|
|
}
|
|
});
|
|
}
|
|
|
|
function sortTable(table, columnIndex, direction, options, persist) {
|
|
const tbody = table.querySelector('tbody');
|
|
if (!tbody) return;
|
|
|
|
const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.children.length > 0 && !row.hasAttribute('data-empty-row'));
|
|
if (!rows.length) return;
|
|
|
|
rows.sort((rowA, rowB) => {
|
|
const cellA = rowA.children[columnIndex];
|
|
const cellB = rowB.children[columnIndex];
|
|
const rawA = cellA ? (cellA.getAttribute('data-sort-value') || cellA.textContent) : '';
|
|
const rawB = cellB ? (cellB.getAttribute('data-sort-value') || cellB.textContent) : '';
|
|
const valueA = parseSortValue(rawA);
|
|
const valueB = parseSortValue(rawB);
|
|
|
|
let comparison = 0;
|
|
if (valueA.type === 'number' && valueB.type === 'number') {
|
|
comparison = valueA.value - valueB.value;
|
|
} else {
|
|
comparison = String(valueA.value).localeCompare(String(valueB.value), undefined, {
|
|
numeric: true,
|
|
sensitivity: 'base',
|
|
});
|
|
}
|
|
return direction === 'asc' ? comparison : -comparison;
|
|
});
|
|
|
|
rows.forEach((row) => tbody.appendChild(row));
|
|
setHeaderState(table, columnIndex, direction);
|
|
if (persist !== false) {
|
|
saveSort(table, columnIndex, direction, options);
|
|
}
|
|
}
|
|
|
|
function getInitialSort(table, options) {
|
|
const saved = loadSort(table, options);
|
|
if (saved) return saved;
|
|
if (options && options.initialSort) {
|
|
return {
|
|
column: Number(options.initialSort.column || 0),
|
|
direction: options.initialSort.direction === 'desc' ? 'desc' : 'asc',
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function attachHeader(header, table, columnIndex, options) {
|
|
if (header.getAttribute('data-sortable') === 'false') return;
|
|
header.classList.add('sortable');
|
|
header.tabIndex = 0;
|
|
header.addEventListener('click', function () {
|
|
const current = loadSort(table, options);
|
|
const nextDirection = current && current.column === columnIndex && current.direction === 'asc' ? 'desc' : 'asc';
|
|
sortTable(table, columnIndex, nextDirection, options, true);
|
|
});
|
|
header.addEventListener('keydown', function (event) {
|
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
event.preventDefault();
|
|
header.click();
|
|
});
|
|
}
|
|
|
|
function init(target, options = {}) {
|
|
const table = getTable(target);
|
|
if (!table || table.dataset.sortableTableInit === '1') return table;
|
|
|
|
Array.from(table.querySelectorAll('thead th')).forEach((header, index) => attachHeader(header, table, index, options));
|
|
table.dataset.sortableTableInit = '1';
|
|
|
|
const initialSort = getInitialSort(table, options);
|
|
if (initialSort) {
|
|
sortTable(table, initialSort.column, initialSort.direction, options, false);
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
function initAll(selector = '.u-sortable-table') {
|
|
Array.from(document.querySelectorAll(selector)).forEach((table) => init(table));
|
|
}
|
|
|
|
return {
|
|
init,
|
|
initAll,
|
|
parseSortValue,
|
|
sortTable,
|
|
};
|
|
})); |