Web changes: Split shared helpers, bootstrap logic, route groups, and upload-sync behavior out of web.js. Kept web.js focused on wiring and server startup. Fixed screen playlist reassignment so changing a screen’s playlist now triggers a refresh. Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide. Player changes: Split websocket/runtime handling into runtime.js. Split playlist assembly and revision hashing into playlist.js. Split onboarding and player HTTP routes into dedicated modules. Split render utilities and template loading into render-helpers.js. Kept player.js mostly as startup/orchestration. Validation: Rebuilt both services with Docker Compose. Smoke-checked web and player routes after the refactor. Verified get_errors was clean on the touched modules.
238 lines
7.6 KiB
JavaScript
238 lines
7.6 KiB
JavaScript
(function () {
|
|
function getTableHeaderText(headerCell) {
|
|
return String(headerCell && headerCell.textContent ? headerCell.textContent : '').trim().toLowerCase();
|
|
}
|
|
|
|
function cellHasButtonContent(cell) {
|
|
return Boolean(cell && cell.querySelector && cell.querySelector('button, form, input, select, textarea'));
|
|
}
|
|
|
|
function getCellSortValue(cell) {
|
|
if (!cell) {
|
|
return '';
|
|
}
|
|
var sortValue = cell.getAttribute && cell.getAttribute('data-sort-value');
|
|
if (sortValue !== null && sortValue !== undefined && sortValue !== '') {
|
|
return String(sortValue).trim();
|
|
}
|
|
return String(cell.textContent || '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function getComparableSortValue(rawValue) {
|
|
var value = String(rawValue || '').trim();
|
|
if (!value) {
|
|
return { type: 'empty', value: '' };
|
|
}
|
|
|
|
var numericValue = Number(value.replace(/,/g, ''));
|
|
if (!Number.isNaN(numericValue) && value !== '') {
|
|
return { type: 'number', value: numericValue };
|
|
}
|
|
|
|
var dateValue = Date.parse(value);
|
|
if (!Number.isNaN(dateValue)) {
|
|
return { type: 'date', value: dateValue };
|
|
}
|
|
|
|
return { type: 'string', value: value.toLowerCase() };
|
|
}
|
|
|
|
function compareSortValues(leftValue, rightValue) {
|
|
if (leftValue.type === 'empty' && rightValue.type === 'empty') {
|
|
return 0;
|
|
}
|
|
if (leftValue.type === 'empty') {
|
|
return 1;
|
|
}
|
|
if (rightValue.type === 'empty') {
|
|
return -1;
|
|
}
|
|
if (leftValue.type === rightValue.type) {
|
|
if (leftValue.value < rightValue.value) {
|
|
return -1;
|
|
}
|
|
if (leftValue.value > rightValue.value) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
return String(leftValue.value).localeCompare(String(rightValue.value), undefined, { numeric: true, sensitivity: 'base' });
|
|
}
|
|
|
|
function getSortableTableState(table) {
|
|
if (!table._sortableState) {
|
|
table._sortableState = {
|
|
columnIndex: null,
|
|
direction: 'asc'
|
|
};
|
|
}
|
|
return table._sortableState;
|
|
}
|
|
|
|
function ensureSortableHeaderIndicator(headerCell) {
|
|
var indicator = headerCell.querySelector && headerCell.querySelector('.table-sort-indicator');
|
|
if (indicator) {
|
|
return indicator;
|
|
}
|
|
|
|
indicator = document.createElement('i');
|
|
indicator.className = 'table-sort-indicator bi bi-arrow-down-up ms-1';
|
|
indicator.setAttribute('aria-hidden', 'true');
|
|
headerCell.appendChild(indicator);
|
|
return indicator;
|
|
}
|
|
|
|
function updateSortableHeaderIndicator(headerCell, isSortable, isActive, direction) {
|
|
if (!isSortable) {
|
|
var hiddenIndicator = headerCell.querySelector && headerCell.querySelector('.table-sort-indicator');
|
|
if (hiddenIndicator) {
|
|
hiddenIndicator.style.display = 'none';
|
|
}
|
|
return;
|
|
}
|
|
|
|
var indicator = ensureSortableHeaderIndicator(headerCell);
|
|
indicator.style.display = '';
|
|
indicator.className = 'table-sort-indicator bi ms-1';
|
|
|
|
if (isActive && direction === 'desc') {
|
|
indicator.classList.add('bi-caret-down-fill');
|
|
return;
|
|
}
|
|
|
|
if (isActive && direction === 'asc') {
|
|
indicator.classList.add('bi-caret-up-fill');
|
|
return;
|
|
}
|
|
|
|
indicator.classList.add('bi-arrow-down-up');
|
|
}
|
|
|
|
function isSortableTableColumn(table, columnIndex) {
|
|
var headerCell = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0].cells[columnIndex] : null;
|
|
if (!headerCell) {
|
|
return false;
|
|
}
|
|
|
|
if (/actions?|buttons?/i.test(getTableHeaderText(headerCell))) {
|
|
return false;
|
|
}
|
|
|
|
var bodies = table.tBodies ? Array.prototype.slice.call(table.tBodies) : [];
|
|
for (var i = 0; i < bodies.length; i += 1) {
|
|
var rows = Array.prototype.slice.call(bodies[i].rows || []);
|
|
for (var j = 0; j < rows.length; j += 1) {
|
|
var cell = rows[j].cells ? rows[j].cells[columnIndex] : null;
|
|
if (cell && cellHasButtonContent(cell)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function updateSortableHeaderState(table) {
|
|
var state = getSortableTableState(table);
|
|
var headerCells = table.tHead && table.tHead.rows && table.tHead.rows.length ? Array.prototype.slice.call(table.tHead.rows[0].cells || []) : [];
|
|
headerCells.forEach(function (headerCell, index) {
|
|
if (!headerCell) {
|
|
return;
|
|
}
|
|
var sortable = isSortableTableColumn(table, index);
|
|
headerCell.classList.remove('sort-asc', 'sort-desc', 'sortable', 'unsortable');
|
|
headerCell.removeAttribute('aria-sort');
|
|
headerCell.removeAttribute('role');
|
|
headerCell.removeAttribute('tabindex');
|
|
|
|
if (sortable) {
|
|
headerCell.classList.add('sortable');
|
|
updateSortableHeaderIndicator(headerCell, true, state.columnIndex === index, state.direction);
|
|
headerCell.setAttribute('role', 'button');
|
|
headerCell.setAttribute('tabindex', '0');
|
|
if (state.columnIndex === index) {
|
|
headerCell.classList.add(state.direction === 'desc' ? 'sort-desc' : 'sort-asc');
|
|
headerCell.setAttribute('aria-sort', state.direction === 'desc' ? 'descending' : 'ascending');
|
|
} else {
|
|
headerCell.setAttribute('aria-sort', 'none');
|
|
}
|
|
} else {
|
|
headerCell.classList.add('unsortable');
|
|
updateSortableHeaderIndicator(headerCell, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function sortTable(table, columnIndex, direction) {
|
|
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
|
|
if (!tbody) {
|
|
return;
|
|
}
|
|
|
|
var rows = Array.prototype.slice.call(tbody.rows || []);
|
|
if (!rows.length) {
|
|
return;
|
|
}
|
|
|
|
var multiplier = direction === 'desc' ? -1 : 1;
|
|
rows.sort(function (leftRow, rightRow) {
|
|
var leftCell = leftRow.cells ? leftRow.cells[columnIndex] : null;
|
|
var rightCell = rightRow.cells ? rightRow.cells[columnIndex] : null;
|
|
var leftComparable = getComparableSortValue(getCellSortValue(leftCell));
|
|
var rightComparable = getComparableSortValue(getCellSortValue(rightCell));
|
|
return compareSortValues(leftComparable, rightComparable) * multiplier;
|
|
});
|
|
|
|
rows.forEach(function (row) {
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function applyTableSort(table) {
|
|
var state = getSortableTableState(table);
|
|
if (state.columnIndex === null || state.columnIndex === undefined) {
|
|
return;
|
|
}
|
|
sortTable(table, state.columnIndex, state.direction);
|
|
updateSortableHeaderState(table);
|
|
}
|
|
|
|
function initSortableTables() {
|
|
var tables = Array.prototype.slice.call(document.querySelectorAll('table'));
|
|
tables.forEach(function (table) {
|
|
var headerRow = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0] : null;
|
|
if (!headerRow) {
|
|
return;
|
|
}
|
|
|
|
Array.prototype.forEach.call(headerRow.cells, function (headerCell, index) {
|
|
if (!isSortableTableColumn(table, index)) {
|
|
return;
|
|
}
|
|
|
|
ensureSortableHeaderIndicator(headerCell);
|
|
|
|
headerCell.addEventListener('click', function () {
|
|
var state = getSortableTableState(table);
|
|
var nextDirection = state.columnIndex === index && state.direction === 'asc' ? 'desc' : 'asc';
|
|
state.columnIndex = index;
|
|
state.direction = nextDirection;
|
|
sortTable(table, index, nextDirection);
|
|
updateSortableHeaderState(table);
|
|
});
|
|
|
|
headerCell.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
headerCell.click();
|
|
}
|
|
});
|
|
});
|
|
|
|
updateSortableHeaderState(table);
|
|
});
|
|
}
|
|
|
|
window.applyTableSort = applyTableSort;
|
|
window.initSortableTables = initSortableTables;
|
|
}()); |