Release 1.5.13
This commit is contained in:
@@ -267,7 +267,7 @@
|
||||
}
|
||||
|
||||
.dashboard-actions-card .card-header,
|
||||
.dashboard-table-card .card-header {
|
||||
.dashboard-screen-card-shell .card-header {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
@@ -309,6 +309,107 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dashboard-screen-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard-screen-card-header .dashboard-card-heading {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-screen-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-screen-tile {
|
||||
display: grid;
|
||||
gap: 0.95rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: calc(var(--bs-border-radius) + 0.25rem);
|
||||
background: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.dashboard-screen-tile-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-screen-tile-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-screen-name {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.dashboard-screen-link {
|
||||
display: inline-block;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.875rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-screen-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-screen-pill.is-live {
|
||||
background: rgba(25, 135, 84, 0.12);
|
||||
color: var(--bs-success);
|
||||
}
|
||||
|
||||
.dashboard-screen-pill.is-idle {
|
||||
background: rgba(108, 117, 125, 0.12);
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.dashboard-screen-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-screen-meta div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-screen-meta dt {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dashboard-screen-meta dd {
|
||||
margin: 0.15rem 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-screen-empty {
|
||||
margin: 0;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 19 KiB |
@@ -164,9 +164,6 @@
|
||||
if (typeof window.initTableSearches === 'function') {
|
||||
window.initTableSearches(targetElement);
|
||||
}
|
||||
if (typeof window.initTablePaginations === 'function') {
|
||||
window.initTablePaginations(targetElement);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceRefreshTargetFromPage(refreshTargetSelector, sequenceId) {
|
||||
@@ -201,7 +198,7 @@
|
||||
|
||||
currentTarget.outerHTML = nextTarget.outerHTML;
|
||||
rebindRefreshTarget(document.querySelector(refreshTargetSelector));
|
||||
}).catch(function (_error) {
|
||||
}).catch(function () {
|
||||
// Ignore refresh replacement failures and leave the existing content in place.
|
||||
});
|
||||
}
|
||||
@@ -515,303 +512,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initTablePaginations(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
|
||||
Array.prototype.forEach.call(scope.querySelectorAll('[data-table-pagination="auto"]'), function (container) {
|
||||
if (container.getAttribute('data-table-pagination-bound') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
var table = container.querySelector('[data-table-searchable]');
|
||||
var cardHeader = container.querySelector('.card-header');
|
||||
var cardBody = container.querySelector('.card-body.table-responsive') || container.querySelector('.card-body');
|
||||
var footer = container.querySelector('[data-table-pagination-controls]');
|
||||
var footerSummary = null;
|
||||
var footerList = null;
|
||||
var syncFrame = 0;
|
||||
var state = {
|
||||
currentPage: 1,
|
||||
pageSize: 1,
|
||||
totalPages: 1
|
||||
};
|
||||
|
||||
if (!table || !cardBody || !table.tBodies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.setAttribute('data-table-pagination-bound', 'true');
|
||||
|
||||
function getRows() {
|
||||
return Array.prototype.slice.call(table.querySelectorAll('tbody tr[data-table-search-row]'));
|
||||
}
|
||||
|
||||
function isSearchMatched(row) {
|
||||
return String(row.getAttribute('data-table-search-match') || 'true') !== 'false';
|
||||
}
|
||||
|
||||
function ensureFooter() {
|
||||
if (footer) {
|
||||
footerSummary = footer.querySelector('[data-table-pagination-summary]');
|
||||
footerList = footer.querySelector('[data-table-pagination-list]');
|
||||
return footer;
|
||||
}
|
||||
|
||||
footer = document.createElement('div');
|
||||
footer.className = 'card-footer d-none';
|
||||
footer.setAttribute('data-table-pagination-controls', 'true');
|
||||
footer.innerHTML = '' +
|
||||
'<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">' +
|
||||
'<div class="text-muted small" data-table-pagination-summary></div>' +
|
||||
'<nav aria-label="Table pages">' +
|
||||
'<ul class="pagination pagination-sm mb-0" data-table-pagination-list></ul>' +
|
||||
'</nav>' +
|
||||
'</div>';
|
||||
cardBody.parentNode.insertBefore(footer, cardBody.nextSibling);
|
||||
footerSummary = footer.querySelector('[data-table-pagination-summary]');
|
||||
footerList = footer.querySelector('[data-table-pagination-list]');
|
||||
return footer;
|
||||
}
|
||||
|
||||
function setRowVisible(row, visible) {
|
||||
row.hidden = !visible;
|
||||
row.classList.toggle('d-none', !visible);
|
||||
}
|
||||
|
||||
function getAvailableHeight() {
|
||||
var containerRect = container.getBoundingClientRect();
|
||||
var headerHeight = cardHeader ? cardHeader.getBoundingClientRect().height : 0;
|
||||
var footerEstimate = footer && !footer.classList.contains('d-none') ? footer.getBoundingClientRect().height : 56;
|
||||
return Math.max(0, window.innerHeight - Math.max(0, containerRect.top) - headerHeight - footerEstimate - 24);
|
||||
}
|
||||
|
||||
function renderFooter(totalItems) {
|
||||
var pageButtons = [];
|
||||
|
||||
ensureFooter();
|
||||
|
||||
if (state.totalPages <= 1 || totalItems <= 0) {
|
||||
footer.classList.add('d-none');
|
||||
footerSummary.textContent = '';
|
||||
footerList.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
footer.classList.remove('d-none');
|
||||
footerSummary.textContent = 'Showing ' + (((state.currentPage - 1) * state.pageSize) + 1) + '-' + Math.min(totalItems, state.currentPage * state.pageSize) + ' of ' + totalItems;
|
||||
|
||||
pageButtons.push('<li class="page-item' + (state.currentPage <= 1 ? ' disabled' : '') + '"><a class="page-link" href="#" data-table-pagination-page="' + (state.currentPage - 1) + '" aria-label="Previous page">Previous</a></li>');
|
||||
for (var pageNumber = 1; pageNumber <= state.totalPages; pageNumber += 1) {
|
||||
pageButtons.push('<li class="page-item' + (pageNumber === state.currentPage ? ' active' : '') + '"><a class="page-link" href="#" data-table-pagination-page="' + pageNumber + '">' + pageNumber + '</a></li>');
|
||||
}
|
||||
pageButtons.push('<li class="page-item' + (state.currentPage >= state.totalPages ? ' disabled' : '') + '"><a class="page-link" href="#" data-table-pagination-page="' + (state.currentPage + 1) + '" aria-label="Next page">Next</a></li>');
|
||||
footerList.innerHTML = pageButtons.join('');
|
||||
}
|
||||
|
||||
function applyPagination(resetPage) {
|
||||
var rows = getRows();
|
||||
var matchedRows = rows.filter(isSearchMatched);
|
||||
var totalItems = matchedRows.length;
|
||||
var rowHeight = 48;
|
||||
var pageSize;
|
||||
var startIndex;
|
||||
var endIndex;
|
||||
|
||||
if (resetPage) {
|
||||
state.currentPage = 1;
|
||||
}
|
||||
|
||||
rows.forEach(function (row) {
|
||||
if (!isSearchMatched(row)) {
|
||||
setRowVisible(row, false);
|
||||
}
|
||||
});
|
||||
|
||||
if (!totalItems) {
|
||||
state.pageSize = 1;
|
||||
state.totalPages = 1;
|
||||
state.currentPage = 1;
|
||||
renderFooter(0);
|
||||
return;
|
||||
}
|
||||
|
||||
matchedRows.forEach(function (row) {
|
||||
setRowVisible(row, true);
|
||||
});
|
||||
|
||||
if (matchedRows[0]) {
|
||||
rowHeight = Math.max(24, matchedRows[0].getBoundingClientRect().height || matchedRows[0].offsetHeight || 48);
|
||||
}
|
||||
|
||||
pageSize = Math.max(1, Math.floor(getAvailableHeight() / rowHeight));
|
||||
|
||||
if (pageSize >= totalItems) {
|
||||
state.pageSize = totalItems;
|
||||
state.totalPages = 1;
|
||||
state.currentPage = 1;
|
||||
matchedRows.forEach(function (row) {
|
||||
setRowVisible(row, true);
|
||||
});
|
||||
renderFooter(totalItems);
|
||||
return;
|
||||
}
|
||||
|
||||
state.pageSize = pageSize;
|
||||
state.totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
|
||||
state.currentPage = Math.min(Math.max(1, state.currentPage), state.totalPages);
|
||||
startIndex = (state.currentPage - 1) * pageSize;
|
||||
endIndex = startIndex + pageSize;
|
||||
|
||||
matchedRows.forEach(function (row, index) {
|
||||
setRowVisible(row, index >= startIndex && index < endIndex);
|
||||
});
|
||||
|
||||
renderFooter(totalItems);
|
||||
}
|
||||
|
||||
function scheduleSync(options) {
|
||||
if (syncFrame) {
|
||||
window.cancelAnimationFrame(syncFrame);
|
||||
}
|
||||
|
||||
syncFrame = window.requestAnimationFrame(function () {
|
||||
syncFrame = 0;
|
||||
applyPagination(Boolean(options && options.resetPage));
|
||||
});
|
||||
}
|
||||
|
||||
ensureFooter();
|
||||
|
||||
if (footer) {
|
||||
footer.addEventListener('click', function (event) {
|
||||
var target = event.target && event.target.closest ? event.target.closest('[data-table-pagination-page]') : null;
|
||||
var pageValue;
|
||||
|
||||
if (!target || target.closest('.disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
pageValue = Math.max(1, Math.floor(Number(target.getAttribute('data-table-pagination-page')) || 1));
|
||||
state.currentPage = pageValue;
|
||||
scheduleSync();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
scheduleSync();
|
||||
});
|
||||
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
try {
|
||||
new ResizeObserver(function () {
|
||||
scheduleSync();
|
||||
}).observe(container);
|
||||
} catch (_error) {
|
||||
// Ignore observer setup failures and rely on window resize.
|
||||
}
|
||||
}
|
||||
|
||||
container.syncTablePagination = scheduleSync;
|
||||
scheduleSync({ resetPage: true });
|
||||
});
|
||||
}
|
||||
|
||||
function initTableSearches(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
|
||||
Array.prototype.forEach.call(scope.querySelectorAll('[data-table-search]'), function (input) {
|
||||
if (input.getAttribute('data-table-search-bound') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = input.closest('[data-table-search-container]') || input.closest('.card') || document;
|
||||
var table = container.querySelector('[data-table-searchable]');
|
||||
var emptyRow = null;
|
||||
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.setAttribute('data-table-search-bound', 'true');
|
||||
|
||||
function getSearchableRows() {
|
||||
return Array.prototype.slice.call(table.querySelectorAll('tbody tr[data-table-search-row]'));
|
||||
}
|
||||
|
||||
function getDefaultEmptyRow() {
|
||||
return table.querySelector('tbody tr[data-table-search-empty-default]');
|
||||
}
|
||||
|
||||
function removeGeneratedEmptyRow() {
|
||||
if (emptyRow && emptyRow.parentNode) {
|
||||
emptyRow.parentNode.removeChild(emptyRow);
|
||||
}
|
||||
emptyRow = null;
|
||||
}
|
||||
|
||||
function ensureGeneratedEmptyRow(message) {
|
||||
var tbody = table.tBodies[0] || table.querySelector('tbody');
|
||||
var columnCount = 1;
|
||||
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emptyRow) {
|
||||
emptyRow = document.createElement('tr');
|
||||
emptyRow.setAttribute('data-table-search-empty-row', 'true');
|
||||
var cell = document.createElement('td');
|
||||
cell.className = 'empty';
|
||||
cell.setAttribute('data-table-search-empty-cell', 'true');
|
||||
emptyRow.appendChild(cell);
|
||||
}
|
||||
|
||||
columnCount = table.tHead && table.tHead.rows && table.tHead.rows[0] ? table.tHead.rows[0].cells.length : (getSearchableRows()[0] ? getSearchableRows()[0].cells.length : 1);
|
||||
emptyRow.firstElementChild.colSpan = Math.max(1, columnCount);
|
||||
emptyRow.firstElementChild.textContent = message;
|
||||
|
||||
if (!emptyRow.parentNode) {
|
||||
tbody.appendChild(emptyRow);
|
||||
}
|
||||
}
|
||||
|
||||
function syncSearch() {
|
||||
var query = String(input.value || '').trim().toLowerCase();
|
||||
var rows = getSearchableRows();
|
||||
var visibleCount = 0;
|
||||
var defaultEmptyRow = getDefaultEmptyRow();
|
||||
|
||||
removeGeneratedEmptyRow();
|
||||
|
||||
rows.forEach(function (row) {
|
||||
var haystack = String(row.getAttribute('data-search-text') || row.textContent || '').toLowerCase();
|
||||
var visible = !query || haystack.indexOf(query) !== -1;
|
||||
row.setAttribute('data-table-search-match', visible ? 'true' : 'false');
|
||||
row.classList.toggle('d-none', !visible);
|
||||
row.hidden = !visible;
|
||||
if (visible) {
|
||||
visibleCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (defaultEmptyRow) {
|
||||
defaultEmptyRow.classList.toggle('d-none', Boolean(query));
|
||||
defaultEmptyRow.hidden = Boolean(query);
|
||||
}
|
||||
|
||||
if (query && visibleCount === 0) {
|
||||
ensureGeneratedEmptyRow('No results match your search.');
|
||||
}
|
||||
|
||||
if (typeof window.syncTablePagination === 'function') {
|
||||
window.syncTablePagination(container, { resetPage: true });
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('input', syncSearch);
|
||||
syncSearch();
|
||||
});
|
||||
if (typeof window.initTableSearches === 'function') {
|
||||
window.initTableSearches(root);
|
||||
}
|
||||
}
|
||||
|
||||
function createSlideThumbPlaceholder() {
|
||||
@@ -841,10 +545,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (window.initSortableTables) {
|
||||
window.initSortableTables();
|
||||
}
|
||||
|
||||
initConfirmForms();
|
||||
initDirtyTracking();
|
||||
initCancelConfirm();
|
||||
@@ -854,19 +554,9 @@
|
||||
initJsonTogglePanels();
|
||||
initLocalDateTimes();
|
||||
initTableSearches();
|
||||
initTablePaginations();
|
||||
attachSlideThumbFallbacks(document);
|
||||
window.initJsonTogglePanels = initJsonTogglePanels;
|
||||
window.initLocalDateTimes = initLocalDateTimes;
|
||||
window.initTableSearches = initTableSearches;
|
||||
window.initTablePaginations = initTablePaginations;
|
||||
window.syncTablePagination = function (container, options) {
|
||||
if (!container || !container.syncTablePagination) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.syncTablePagination(options);
|
||||
};
|
||||
window.createSlideThumbPlaceholder = createSlideThumbPlaceholder;
|
||||
window.attachSlideThumbFallbacks = attachSlideThumbFallbacks;
|
||||
}());
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
(function () {
|
||||
function getGroupCheckboxes(group) {
|
||||
return Array.prototype.slice.call(group.querySelectorAll('input[name="permission_keys[]"]'));
|
||||
}
|
||||
|
||||
function getPermissionKey(checkbox) {
|
||||
return String((checkbox && (checkbox.getAttribute('data-permission-key') || checkbox.value)) || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getActionKey(checkbox) {
|
||||
var permissionKey = getPermissionKey(checkbox);
|
||||
var parts = permissionKey.split('.');
|
||||
if (parts.length !== 2) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(parts[1] || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function syncPermissionGroup(group) {
|
||||
var checkboxes = getGroupCheckboxes(group);
|
||||
if (!checkboxes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var readCheckbox = null;
|
||||
var nonReadChecked = false;
|
||||
|
||||
checkboxes.forEach(function (checkbox) {
|
||||
if (getActionKey(checkbox) === 'read') {
|
||||
readCheckbox = checkbox;
|
||||
return;
|
||||
}
|
||||
if (checkbox.checked) {
|
||||
nonReadChecked = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!readCheckbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readCheckbox.checked && nonReadChecked) {
|
||||
readCheckbox.checked = true;
|
||||
}
|
||||
|
||||
if (!readCheckbox.checked) {
|
||||
checkboxes.forEach(function (checkbox) {
|
||||
if (getActionKey(checkbox) !== 'read') {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleGroupChange(event) {
|
||||
var checkbox = event.target && event.target.matches ? event.target : null;
|
||||
if (!checkbox || checkbox.tagName !== 'INPUT' || checkbox.type !== 'checkbox') {
|
||||
return;
|
||||
}
|
||||
|
||||
var actionKey = getActionKey(checkbox);
|
||||
var group = checkbox.closest('.accordion-item');
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
var checkboxes = getGroupCheckboxes(group);
|
||||
var readCheckbox = checkboxes.find(function (candidate) {
|
||||
return getActionKey(candidate) === 'read';
|
||||
}) || null;
|
||||
|
||||
if (!readCheckbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionKey === 'read' && !checkbox.checked) {
|
||||
checkboxes.forEach(function (candidate) {
|
||||
if (candidate !== checkbox) {
|
||||
candidate.checked = false;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionKey !== 'read' && checkbox.checked) {
|
||||
readCheckbox.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
function initPermissionGroups() {
|
||||
document.querySelectorAll('.accordion-item').forEach(function (group) {
|
||||
syncPermissionGroup(group);
|
||||
});
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
var checkbox = event.target;
|
||||
if (!checkbox || checkbox.tagName !== 'INPUT' || checkbox.type !== 'checkbox') {
|
||||
return;
|
||||
}
|
||||
if (String(checkbox.getAttribute('name') || '') !== 'permission_keys[]') {
|
||||
return;
|
||||
}
|
||||
|
||||
handleGroupChange(event);
|
||||
syncPermissionGroup(checkbox.closest('.accordion-item'));
|
||||
});
|
||||
}
|
||||
|
||||
initPermissionGroups();
|
||||
}());
|
||||
@@ -1,102 +0,0 @@
|
||||
(function () {
|
||||
function updateSidebarStatus(status, label) {
|
||||
var dot = document.getElementById('sidebar-status-dot');
|
||||
var text = document.getElementById('sidebar-status-text');
|
||||
var pill = document.getElementById('sidebar-status-pill');
|
||||
if (!dot && !text && !pill) {
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedStatus = status === 'online' || status === 'offline' || status === 'unknown'
|
||||
? status
|
||||
: (status ? 'online' : 'offline');
|
||||
var statusLabel = normalizedStatus === 'online'
|
||||
? (label || 'Connected to player feed')
|
||||
: normalizedStatus === 'offline'
|
||||
? 'Disconnected from player feed'
|
||||
: (label || 'Connecting to player feed');
|
||||
var pillLabel = normalizedStatus === 'online' ? 'Connected' : normalizedStatus === 'offline' ? 'Disconnected' : 'Connecting';
|
||||
|
||||
dot.classList.remove('status-dot--unknown', 'status-dot--online', 'status-dot--offline');
|
||||
dot.classList.add(normalizedStatus === 'online' ? 'status-dot--online' : normalizedStatus === 'offline' ? 'status-dot--offline' : 'status-dot--unknown');
|
||||
dot.setAttribute('aria-label', statusLabel);
|
||||
dot.setAttribute('title', statusLabel);
|
||||
|
||||
if (text) {
|
||||
text.textContent = statusLabel;
|
||||
}
|
||||
|
||||
if (pill) {
|
||||
pill.classList.remove('status-pill--unknown', 'status-pill--online', 'status-pill--offline');
|
||||
pill.classList.add(normalizedStatus === 'online' ? 'status-pill--online' : normalizedStatus === 'offline' ? 'status-pill--offline' : 'status-pill--unknown');
|
||||
pill.textContent = pillLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function connectDashboardSocket() {
|
||||
if (!window.WebSocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hasStatusIndicators = document.getElementById('sidebar-status-dot') || document.getElementById('sidebar-status-text') || document.getElementById('sidebar-status-pill');
|
||||
if (!hasStatusIndicators) {
|
||||
return;
|
||||
}
|
||||
|
||||
var socket = null;
|
||||
var reconnectTimer = null;
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
reconnectTimer = window.setTimeout(function () {
|
||||
reconnectTimer = null;
|
||||
connect();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
socket = new WebSocket(protocol + '//' + window.location.host + '/ws/dashboard');
|
||||
updateSidebarStatus('unknown', 'Connecting to player feed');
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
try {
|
||||
var payload = JSON.parse(String(event.data || '{}'));
|
||||
if (payload && payload.type === 'dashboard-state') {
|
||||
if (typeof window.webHandleDashboardState === 'function') {
|
||||
window.webHandleDashboardState(payload.state);
|
||||
}
|
||||
updateSidebarStatus(Boolean(payload.state && payload.state.playerServiceConnected), 'Live player feed');
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore malformed dashboard payloads.
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = function () {
|
||||
updateSidebarStatus('offline');
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = function () {
|
||||
updateSidebarStatus('offline');
|
||||
try {
|
||||
socket.close();
|
||||
} catch (_error) {
|
||||
// ignore close errors
|
||||
}
|
||||
};
|
||||
|
||||
socket.onopen = function () {
|
||||
updateSidebarStatus('unknown', 'Connecting to player feed');
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
connectDashboardSocket();
|
||||
}());
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
(function () {
|
||||
var THEME_STORAGE_KEY = 'web-theme';
|
||||
var CKEDITOR_THEME_STYLE_ID = 'ckeditor-dark-theme-overrides';
|
||||
|
||||
function getPreferredTheme() {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function getStoredTheme() {
|
||||
try {
|
||||
var storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (storedTheme === 'dark' || storedTheme === 'light') {
|
||||
return storedTheme;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setStoredTheme(theme) {
|
||||
try {
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getOrCreateCkeditorThemeStyleElement() {
|
||||
var styleElement = document.getElementById(CKEDITOR_THEME_STYLE_ID);
|
||||
|
||||
if (styleElement) {
|
||||
return styleElement;
|
||||
}
|
||||
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = CKEDITOR_THEME_STYLE_ID;
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
return styleElement;
|
||||
}
|
||||
|
||||
function syncCkeditorTheme(theme) {
|
||||
var styleElement = getOrCreateCkeditorThemeStyleElement();
|
||||
|
||||
if (theme !== 'dark') {
|
||||
styleElement.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
styleElement.textContent = [
|
||||
'.ck-body-wrapper .ck.ck-dropdown__panel,',
|
||||
'.ck-body-wrapper .ck.ck-list__panel,',
|
||||
'.ck-body-wrapper .ck.ck-list,',
|
||||
'.ck-body-wrapper .ck.ck-balloon-panel {',
|
||||
' background: var(--bs-body-bg) !important;',
|
||||
' background-color: var(--bs-body-bg) !important;',
|
||||
' border-color: var(--bs-border-color) !important;',
|
||||
' color: var(--bs-body-color) !important;',
|
||||
'}',
|
||||
'.ck-body-wrapper .ck.ck-list .ck-list-item-button {',
|
||||
' background: transparent !important;',
|
||||
' background-color: transparent !important;',
|
||||
' color: var(--bs-body-color) !important;',
|
||||
'}',
|
||||
'.ck-body-wrapper .ck.ck-list .ck-list-item-button:hover {',
|
||||
' background: var(--bs-secondary-bg) !important;',
|
||||
' background-color: var(--bs-secondary-bg) !important;',
|
||||
'}',
|
||||
'.ck-body-wrapper .ck.ck-color-grid,',
|
||||
'.ck-body-wrapper .ck.ck-color-grid__tile {',
|
||||
' color: var(--bs-body-color) !important;',
|
||||
'}',
|
||||
'.ck-body-wrapper .ck.ck-color-grid__tile {',
|
||||
' border-color: var(--bs-border-color) !important;',
|
||||
'}'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
var normalizedTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
var nextTheme = normalizedTheme === 'dark' ? 'light' : 'dark';
|
||||
var nextThemeLabel = nextTheme === 'dark' ? 'Dark mode' : 'Light mode';
|
||||
|
||||
document.documentElement.dataset.bsTheme = normalizedTheme;
|
||||
document.documentElement.style.colorScheme = normalizedTheme;
|
||||
syncCkeditorTheme(normalizedTheme);
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-toggle]'), function (toggleButton) {
|
||||
var icon = toggleButton.querySelector('.theme-toggle__icon');
|
||||
toggleButton.setAttribute('aria-pressed', normalizedTheme === 'dark' ? 'true' : 'false');
|
||||
toggleButton.setAttribute('aria-label', 'Switch to ' + nextThemeLabel.toLowerCase());
|
||||
if (icon) {
|
||||
icon.classList.remove('theme-toggle__icon--moon', 'theme-toggle__icon--sun');
|
||||
icon.classList.add(normalizedTheme === 'dark' ? 'theme-toggle__icon--sun' : 'theme-toggle__icon--moon');
|
||||
}
|
||||
});
|
||||
|
||||
return normalizedTheme;
|
||||
}
|
||||
|
||||
function initThemeToggle() {
|
||||
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-theme-toggle]'));
|
||||
var storedTheme = getStoredTheme();
|
||||
var theme = storedTheme || getPreferredTheme();
|
||||
|
||||
applyTheme(theme);
|
||||
|
||||
if (!toggleButtons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.addEventListener('click', function () {
|
||||
var nextTheme = document.documentElement.dataset.bsTheme === 'dark' ? 'light' : 'dark';
|
||||
setStoredTheme(nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle() {
|
||||
var body = document.body;
|
||||
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-sidebar-toggle]'));
|
||||
var backdrop = document.querySelector('[data-sidebar-backdrop]');
|
||||
|
||||
if (!toggleButtons.length || !backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
function setSidebarOpen(isOpen) {
|
||||
body.classList.toggle('is-sidebar-open', isOpen);
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
setSidebarOpen(!body.classList.contains('is-sidebar-open'));
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.addEventListener('click', function () {
|
||||
toggleSidebar();
|
||||
});
|
||||
});
|
||||
|
||||
backdrop.addEventListener('click', function () {
|
||||
setSidebarOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.getPreferredTheme = getPreferredTheme;
|
||||
window.getStoredTheme = getStoredTheme;
|
||||
window.setStoredTheme = setStoredTheme;
|
||||
window.applyTheme = applyTheme;
|
||||
window.initThemeToggle = initThemeToggle;
|
||||
window.initSidebarToggle = initSidebarToggle;
|
||||
|
||||
initThemeToggle();
|
||||
initSidebarToggle();
|
||||
}());
|
||||
@@ -1,134 +0,0 @@
|
||||
(function () {
|
||||
function getBootstrapToast(toast) {
|
||||
if (!toast || !window.bootstrap || !window.bootstrap.Toast) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.bootstrap.Toast.getOrCreateInstance(toast, {
|
||||
autohide: true,
|
||||
delay: 4000
|
||||
});
|
||||
}
|
||||
|
||||
function removeToast(toast) {
|
||||
if (toast && toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}
|
||||
|
||||
function dismissToast(toast) {
|
||||
var instance = getBootstrapToast(toast);
|
||||
if (instance) {
|
||||
instance.hide();
|
||||
return;
|
||||
}
|
||||
removeToast(toast);
|
||||
}
|
||||
|
||||
function setToastVariant(toast, variant) {
|
||||
if (!toast || !toast.classList) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nextVariant = String(variant || 'info').trim().toLowerCase();
|
||||
var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'];
|
||||
variants.forEach(function (value) {
|
||||
toast.classList.remove('text-bg-' + value);
|
||||
});
|
||||
toast.classList.add('text-bg-' + (variants.indexOf(nextVariant) === -1 ? 'info' : nextVariant));
|
||||
}
|
||||
|
||||
function getMessageVariant(message, fallbackVariant) {
|
||||
var text = String(message || '').trim();
|
||||
if (/\b(?:unable to|cannot|can't|could not|failed to)\s+delete\b/i.test(text) || /\bdelete\b.*\b(?:before|first)\b/i.test(text) || /\bstill (?:in use|linked|assigned|used)\b/i.test(text)) {
|
||||
return 'danger';
|
||||
}
|
||||
if (/\b(?:already exists|already exist|already taken|duplicate|must be unique|name already exists)\b/i.test(text)) {
|
||||
return 'warning';
|
||||
}
|
||||
return String(fallbackVariant || 'info').trim().toLowerCase() || 'info';
|
||||
}
|
||||
|
||||
function showToast(message, variant) {
|
||||
var text = String(message || '').trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = document.getElementById('app-toast-container');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
var existingToast = document.getElementById('app-toast');
|
||||
if (existingToast) {
|
||||
var existingBody = existingToast.querySelector('.toast-body');
|
||||
if (existingBody) {
|
||||
existingBody.textContent = text;
|
||||
}
|
||||
var nextVariant = getMessageVariant(text, variant);
|
||||
existingToast.setAttribute('data-toast-variant', nextVariant);
|
||||
setToastVariant(existingToast, nextVariant);
|
||||
var existingInstance = getBootstrapToast(existingToast);
|
||||
if (existingInstance) {
|
||||
existingInstance.show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center border-0';
|
||||
toast.id = 'app-toast';
|
||||
toast.setAttribute('role', 'status');
|
||||
toast.setAttribute('aria-live', 'polite');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
toast.setAttribute('data-bs-autohide', 'true');
|
||||
toast.setAttribute('data-bs-delay', '4000');
|
||||
toast.innerHTML = '<div class="d-flex"><div class="toast-body"></div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></div>';
|
||||
var toastVariant = getMessageVariant(text, variant);
|
||||
toast.setAttribute('data-toast-variant', toastVariant);
|
||||
setToastVariant(toast, toastVariant);
|
||||
toast.querySelector('.toast-body').textContent = text;
|
||||
|
||||
toast.addEventListener('hidden.bs.toast', function () {
|
||||
removeToast(toast);
|
||||
});
|
||||
|
||||
container.appendChild(toast);
|
||||
var instance = getBootstrapToast(toast);
|
||||
if (instance) {
|
||||
instance.show();
|
||||
}
|
||||
}
|
||||
|
||||
function initToast() {
|
||||
var toast = document.getElementById('app-toast');
|
||||
if (!toast) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var url = new URL(window.location.href);
|
||||
if (url.searchParams.has('message')) {
|
||||
url.searchParams.delete('message');
|
||||
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore URL cleanup failures
|
||||
}
|
||||
|
||||
var existingVariant = String(toast.getAttribute('data-toast-variant') || '').trim().toLowerCase() || getMessageVariant((toast.querySelector('.toast-body') && toast.querySelector('.toast-body').textContent) || '', 'info');
|
||||
setToastVariant(toast, existingVariant);
|
||||
|
||||
var instance = getBootstrapToast(toast);
|
||||
if (instance) {
|
||||
instance.show();
|
||||
}
|
||||
}
|
||||
|
||||
window.dismissToast = dismissToast;
|
||||
window.showToast = showToast;
|
||||
window.initToast = initToast;
|
||||
|
||||
initToast();
|
||||
}());
|
||||
@@ -180,18 +180,50 @@
|
||||
].join('');
|
||||
}
|
||||
|
||||
function renderScreenRow(screen) {
|
||||
function renderScreenTile(screen) {
|
||||
var clientCount = Number(screen.player_connection_count || 0);
|
||||
var playerUrl = String(screen.player_url || '').trim();
|
||||
var connectionLabel = clientCount ? clientCount + ' live' : 'No clients';
|
||||
var connectionStateClass = clientCount ? 'is-live' : 'is-idle';
|
||||
var playlistLabel = screen.playlist_name ? escapeHtml(screen.playlist_name) : 'Unassigned';
|
||||
var connectionsLabel = clientCount ? clientCount + ' connected' : 'No clients connected';
|
||||
|
||||
return [
|
||||
'<tr>',
|
||||
'<td data-label="Name">' + escapeHtml(screen.name) + '</td>',
|
||||
'<td data-label="Player URL"><a href="' + escapeHtml(screen.player_url || '') + '" target="_blank">' + escapeHtml(screen.player_url || '') + '</a></td>',
|
||||
'<td data-label="Playlist">' + escapeHtml(screen.playlist_name || '') + '</td>',
|
||||
'<td data-label="Connected clients">' + (clientCount ? '<div class="connection-count" data-screen-connection-count="' + escapeHtml(screen.slug) + '">' + clientCount + ' connected</div>' : '<span class="empty">No clients connected.</span>') + '</td>',
|
||||
'</tr>'
|
||||
'<article class="dashboard-screen-tile" data-screen-key="' + escapeHtml(screen.id || '') + '">',
|
||||
'<div class="dashboard-screen-tile-top">',
|
||||
'<div class="dashboard-screen-tile-text">',
|
||||
'<h4 class="dashboard-screen-name">' + escapeHtml(screen.name || '') + '</h4>',
|
||||
'<a class="dashboard-screen-link" href="' + escapeHtml(playerUrl) + '" target="_blank">' + escapeHtml(playerUrl) + '</a>',
|
||||
'</div>',
|
||||
'<span class="dashboard-screen-pill ' + connectionStateClass + '">' + escapeHtml(connectionLabel) + '</span>',
|
||||
'</div>',
|
||||
'<dl class="dashboard-screen-meta">',
|
||||
'<div><dt>Playlist</dt><dd>' + playlistLabel + '</dd></div>',
|
||||
'<div><dt>Connections</dt><dd>' + escapeHtml(connectionsLabel) + '</dd></div>',
|
||||
'</dl>',
|
||||
'</article>'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function compareScreensByConnectedClients(left, right) {
|
||||
var leftCount = Number(left && left.player_connection_count || 0);
|
||||
var rightCount = Number(right && right.player_connection_count || 0);
|
||||
|
||||
if (leftCount !== rightCount) {
|
||||
return rightCount - leftCount;
|
||||
}
|
||||
|
||||
var leftName = String(left && left.name || '').trim();
|
||||
var rightName = String(right && right.name || '').trim();
|
||||
var nameCompare = leftName.localeCompare(rightName, undefined, { sensitivity: 'base', numeric: true });
|
||||
|
||||
if (nameCompare !== 0) {
|
||||
return nameCompare;
|
||||
}
|
||||
|
||||
return String(left && left.slug || '').localeCompare(String(right && right.slug || ''), undefined, { sensitivity: 'base', numeric: true });
|
||||
}
|
||||
|
||||
function updateStats(state) {
|
||||
var clientCount = document.getElementById('dashboard-client-count');
|
||||
var screenCount = document.getElementById('dashboard-screen-count');
|
||||
@@ -280,36 +312,52 @@
|
||||
tbody.removeChild(tbody.lastElementChild);
|
||||
}
|
||||
|
||||
window.applyTableSort(document.getElementById('dashboard-clients-table'));
|
||||
}
|
||||
|
||||
function updateScreenTable(state) {
|
||||
var table = document.getElementById('dashboard-screens-table');
|
||||
if (!table || !Array.isArray(state.screens)) {
|
||||
function updateScreenGrid(state) {
|
||||
var grid = document.getElementById('dashboard-screens-grid');
|
||||
if (!grid || !Array.isArray(state.screens)) {
|
||||
return;
|
||||
}
|
||||
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
|
||||
if (!tbody) {
|
||||
var screens = state.screens.slice().sort(compareScreensByConnectedClients);
|
||||
|
||||
if (!screens.length) {
|
||||
grid.innerHTML = '<div class="empty dashboard-screen-empty">No screens yet.</div>';
|
||||
return;
|
||||
}
|
||||
if (!state.screens.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty">No screens yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = state.screens.map(renderScreenRow).join('');
|
||||
window.applyTableSort(table);
|
||||
grid.innerHTML = screens.map(renderScreenTile).join('');
|
||||
}
|
||||
|
||||
function updateDashboardQuickActions(state) {
|
||||
var pauseButton = document.getElementById('dashboard-pause-all-button');
|
||||
var blackoutButton = document.getElementById('dashboard-blackout-all-button');
|
||||
if (!blackoutButton || !state || !Array.isArray(state.clients)) {
|
||||
if ((!pauseButton && !blackoutButton) || !state || !Array.isArray(state.clients)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hasClients = state.clients.length > 0;
|
||||
var allPaused = hasClients && state.clients.every(function (client) {
|
||||
return Boolean(client && client.paused);
|
||||
});
|
||||
var allBlackout = hasClients && state.clients.every(function (client) {
|
||||
return Boolean(client && client.blackout);
|
||||
});
|
||||
if (pauseButton) {
|
||||
var pauseForm = pauseButton.form;
|
||||
var pauseInput = pauseForm ? pauseForm.querySelector('input[name="paused"]') : null;
|
||||
var pauseLabel = allPaused ? 'Resume all clients' : 'Pause all clients';
|
||||
var pauseButtonIcon = allPaused ? 'bi-play-fill' : 'bi-pause-fill';
|
||||
pauseButton.innerHTML = '<i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(pauseLabel);
|
||||
setButtonVariant(pauseButton, ['btn-success', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info');
|
||||
if (pauseInput) {
|
||||
pauseInput.value = allPaused ? 'false' : 'true';
|
||||
}
|
||||
if (pauseForm) {
|
||||
pauseForm.setAttribute('data-confirm-message', allPaused ? 'Resume all connected clients?' : 'Pause all connected clients?');
|
||||
}
|
||||
pauseButton.setAttribute('aria-label', pauseLabel);
|
||||
}
|
||||
|
||||
var label = allBlackout ? 'Restore all clients' : 'Blackout all clients';
|
||||
var blackoutForm = blackoutButton.form;
|
||||
var blackoutInput = blackoutForm ? blackoutForm.querySelector('input[name="blackout"]') : null;
|
||||
@@ -331,7 +379,7 @@
|
||||
return;
|
||||
}
|
||||
updateStats(state);
|
||||
updateScreenTable(state);
|
||||
updateScreenGrid(state);
|
||||
updateClientTable(state);
|
||||
updateDashboardQuickActions(state);
|
||||
}
|
||||
|
||||
@@ -1,247 +1,16 @@
|
||||
(function () {
|
||||
var REFRESH_INTERVAL_MS = 10000;
|
||||
var pathname = window.location.pathname.replace(/\/$/, '');
|
||||
var stateNode = document.getElementById('background-tasks-state');
|
||||
var currentVersion = stateNode ? String(stateNode.getAttribute('data-state-version') || '') : '';
|
||||
|
||||
if (pathname !== '/settings/background-tasks' && pathname !== '/settings/background-tasks/scheduled') {
|
||||
if (window.location.pathname.replace(/\/$/, '') !== '/settings/tasks-background' && window.location.pathname.replace(/\/$/, '') !== '/settings/tasks-scheduled') {
|
||||
return;
|
||||
}
|
||||
|
||||
function getPageUrl(href) {
|
||||
try {
|
||||
return new URL(href, window.location.href);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLocalDateTimes(root) {
|
||||
if (typeof window.initLocalDateTimes === 'function') {
|
||||
window.initLocalDateTimes(root || document);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceSectionFromDocument(sectionName, responseDocument) {
|
||||
var currentCard = document.querySelector('[data-pagination-card="' + sectionName + '"]');
|
||||
var nextCard = responseDocument.querySelector('[data-pagination-card="' + sectionName + '"]');
|
||||
|
||||
if (!currentCard || !nextCard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentCard.outerHTML = nextCard.outerHTML;
|
||||
updateLocalDateTimes(document.querySelector('[data-pagination-card="' + sectionName + '"]'));
|
||||
return true;
|
||||
}
|
||||
|
||||
function replaceSummaryFromDocument(responseDocument) {
|
||||
var currentSummary = document.getElementById('background-tasks-summary');
|
||||
var nextSummary = responseDocument.getElementById('background-tasks-summary');
|
||||
|
||||
if (!currentSummary || !nextSummary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentSummary.outerHTML = nextSummary.outerHTML;
|
||||
return true;
|
||||
}
|
||||
|
||||
function replaceFiltersFromDocument(responseDocument) {
|
||||
var currentFilters = document.getElementById('background-tasks-filters');
|
||||
var nextFilters = responseDocument.getElementById('background-tasks-filters');
|
||||
|
||||
if (!currentFilters || !nextFilters) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentFilters.outerHTML = nextFilters.outerHTML;
|
||||
return true;
|
||||
}
|
||||
|
||||
function replaceStateFromDocument(responseDocument) {
|
||||
var currentState = document.getElementById('background-tasks-state');
|
||||
var nextState = responseDocument.getElementById('background-tasks-state');
|
||||
|
||||
if (!currentState || !nextState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentState.outerHTML = nextState.outerHTML;
|
||||
stateNode = document.getElementById('background-tasks-state');
|
||||
currentVersion = stateNode ? String(stateNode.getAttribute('data-state-version') || '') : currentVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyPageResponse(responseDocument, sectionNames) {
|
||||
var updated = false;
|
||||
|
||||
if (replaceStateFromDocument(responseDocument)) {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (replaceSummaryFromDocument(responseDocument)) {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (replaceFiltersFromDocument(responseDocument)) {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
sectionNames.forEach(function (sectionName) {
|
||||
if (replaceSectionFromDocument(sectionName, responseDocument)) {
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
function loadPage(nextUrl, options) {
|
||||
var url = getPageUrl(nextUrl);
|
||||
var sectionNames = options && options.sectionNames ? options.sectionNames : [];
|
||||
var replaceHistory = Boolean(options && options.replaceHistory);
|
||||
|
||||
if (!url) {
|
||||
try {
|
||||
var url = new URL(window.location.href);
|
||||
if (!url.searchParams.has('message')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/html, application/xhtml+xml',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Unable to load background tasks page.');
|
||||
}
|
||||
return response.text();
|
||||
}).then(function (text) {
|
||||
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
||||
if (!applyPageResponse(responseDocument, sectionNames.length ? sectionNames : ['tasks', 'recurring'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (replaceHistory) {
|
||||
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
|
||||
} else {
|
||||
window.history.pushState({}, document.title, url.pathname + url.search + url.hash);
|
||||
}
|
||||
}).catch(function (_error) {
|
||||
window.location.assign(url.pathname + url.search + url.hash);
|
||||
});
|
||||
url.searchParams.delete('message');
|
||||
window.history.replaceState({}, document.title, url.pathname + (url.search ? url.search : '') + url.hash);
|
||||
} catch (_error) {
|
||||
// Ignore URL cleanup failures.
|
||||
}
|
||||
|
||||
function stripMessageParameter() {
|
||||
try {
|
||||
var url = new URL(window.location.href);
|
||||
if (!url.searchParams.has('message')) {
|
||||
return;
|
||||
}
|
||||
url.searchParams.delete('message');
|
||||
window.history.replaceState({}, document.title, url.pathname + (url.search ? url.search : '') + url.hash);
|
||||
} catch (_error) {
|
||||
// Ignore URL cleanup failures.
|
||||
}
|
||||
}
|
||||
|
||||
function findPaginationLink(target) {
|
||||
if (!target || !target.closest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var link = target.closest('.pagination .page-link');
|
||||
if (!link) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var href = String(link.getAttribute('href') || '').trim();
|
||||
if (!href || href === '#') {
|
||||
return null;
|
||||
}
|
||||
|
||||
var card = link.closest('[data-pagination-card]');
|
||||
if (!card) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
href: href,
|
||||
sectionName: String(card.getAttribute('data-pagination-card') || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function initPaginationNavigation() {
|
||||
document.addEventListener('click', function (event) {
|
||||
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var paginationLink = findPaginationLink(event.target);
|
||||
if (paginationLink && paginationLink.sectionName) {
|
||||
var currentUrl = getPageUrl(window.location.href);
|
||||
var nextUrl = getPageUrl(paginationLink.href);
|
||||
if (!currentUrl || !nextUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentUrl.pathname === nextUrl.pathname && currentUrl.search === nextUrl.search && currentUrl.hash === nextUrl.hash) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
loadPage(nextUrl.toString(), { sectionNames: [paginationLink.sectionName] });
|
||||
return;
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.addEventListener('popstate', function () {
|
||||
loadPage(window.location.href, { replaceHistory: true });
|
||||
});
|
||||
}
|
||||
|
||||
function refreshPage() {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/settings/background-tasks/state', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
}).then(function (payload) {
|
||||
if (!payload || !payload.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nextVersion = String(payload.version || '');
|
||||
if (!currentVersion) {
|
||||
currentVersion = nextVersion;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextVersion !== currentVersion) {
|
||||
window.location.reload();
|
||||
}
|
||||
}).catch(function (_error) {
|
||||
// Ignore refresh probe errors and try again on the next interval.
|
||||
});
|
||||
}
|
||||
|
||||
stripMessageParameter();
|
||||
initPaginationNavigation();
|
||||
window.setInterval(refreshPage, REFRESH_INTERVAL_MS);
|
||||
}());
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,242 +0,0 @@
|
||||
(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 (headerCell.getAttribute && headerCell.getAttribute('data-sortable') === 'false') {
|
||||
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;
|
||||
}());
|
||||
@@ -0,0 +1,170 @@
|
||||
(function () {
|
||||
function rebindTableContainer(container) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.initSortableTables === 'function') {
|
||||
window.initSortableTables(container);
|
||||
}
|
||||
|
||||
if (typeof window.initTableSearches === 'function') {
|
||||
window.initTableSearches(container);
|
||||
}
|
||||
|
||||
if (typeof window.initLocalDateTimes === 'function') {
|
||||
window.initLocalDateTimes(container);
|
||||
}
|
||||
}
|
||||
|
||||
function focusSearchInput(input) {
|
||||
if (!input || !input.focus) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.focus();
|
||||
|
||||
if (typeof input.setSelectionRange === 'function') {
|
||||
var valueLength = String(input.value || '').length;
|
||||
try {
|
||||
input.setSelectionRange(valueLength, valueLength);
|
||||
} catch (_error) {
|
||||
// Ignore selection failures on unsupported input types.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceTableResults(container, responseDocument) {
|
||||
if (!container || !responseDocument || !responseDocument.querySelector) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentTable = container.querySelector('[data-table-searchable]');
|
||||
var nextContainer = responseDocument.querySelector('[data-table-search-container]') || responseDocument.querySelector('.card');
|
||||
var nextTable = nextContainer ? nextContainer.querySelector('[data-table-searchable]') : null;
|
||||
var currentFooter = container.querySelector('.card-footer');
|
||||
var nextFooter = nextContainer ? nextContainer.querySelector('.card-footer') : null;
|
||||
|
||||
if (!currentTable || !nextTable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentBody = currentTable.tBodies && currentTable.tBodies[0] ? currentTable.tBodies[0] : null;
|
||||
var nextBody = nextTable.tBodies && nextTable.tBodies[0] ? nextTable.tBodies[0] : null;
|
||||
|
||||
if (!currentBody || !nextBody) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentBody.outerHTML = nextBody.outerHTML;
|
||||
|
||||
if (currentFooter && nextFooter) {
|
||||
currentFooter.outerHTML = nextFooter.outerHTML;
|
||||
} else if (currentFooter && !nextFooter) {
|
||||
currentFooter.parentNode.removeChild(currentFooter);
|
||||
} else if (!currentFooter && nextFooter) {
|
||||
currentTable.parentNode.appendChild(nextFooter.cloneNode(true));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function initTableSearches(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
|
||||
Array.prototype.forEach.call(scope.querySelectorAll('[data-table-search]'), function (input) {
|
||||
if (input.getAttribute('data-table-search-bound') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
input.setAttribute('data-table-search-bound', 'true');
|
||||
|
||||
var searchParam = String(input.getAttribute('data-table-search-param') || 'search').trim() || 'search';
|
||||
var currentUrl = new URL(window.location.href);
|
||||
var pendingSearchTimer = null;
|
||||
var requestSequence = 0;
|
||||
var container = input.closest('[data-table-search-container]') || input.closest('.card') || null;
|
||||
|
||||
input.value = String(currentUrl.searchParams.get(searchParam) || '').trim();
|
||||
|
||||
function updateSearch() {
|
||||
var query = String(input.value || '').trim();
|
||||
var nextUrl = new URL(window.location.href);
|
||||
|
||||
if (query) {
|
||||
nextUrl.searchParams.set(searchParam, query);
|
||||
} else {
|
||||
nextUrl.searchParams.delete(searchParam);
|
||||
}
|
||||
nextUrl.searchParams.delete('page');
|
||||
|
||||
if (nextUrl.toString() === currentUrl.toString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestSequence += 1;
|
||||
var sequenceId = requestSequence;
|
||||
|
||||
fetch(nextUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/html, application/xhtml+xml',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Unable to load search results.');
|
||||
}
|
||||
return response.text();
|
||||
}).then(function (text) {
|
||||
if (sequenceId !== requestSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
||||
|
||||
if (window.history && window.history.replaceState) {
|
||||
window.history.replaceState({}, document.title, nextUrl.pathname + nextUrl.search + nextUrl.hash);
|
||||
}
|
||||
|
||||
if (replaceTableResults(container, responseDocument)) {
|
||||
currentUrl = nextUrl;
|
||||
rebindTableContainer(container || document);
|
||||
focusSearchInput(input);
|
||||
}
|
||||
}).catch(function () {
|
||||
window.location.assign(nextUrl.toString());
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleSearchUpdate() {
|
||||
if (pendingSearchTimer) {
|
||||
window.clearTimeout(pendingSearchTimer);
|
||||
}
|
||||
pendingSearchTimer = window.setTimeout(function () {
|
||||
pendingSearchTimer = null;
|
||||
updateSearch();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
input.addEventListener('input', scheduleSearchUpdate);
|
||||
input.addEventListener('search', scheduleSearchUpdate);
|
||||
input.addEventListener('change', updateSearch);
|
||||
input.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (pendingSearchTimer) {
|
||||
window.clearTimeout(pendingSearchTimer);
|
||||
pendingSearchTimer = null;
|
||||
}
|
||||
updateSearch();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.initTableSearches = initTableSearches;
|
||||
window.replaceTableResults = replaceTableResults;
|
||||
}());
|
||||
@@ -0,0 +1,197 @@
|
||||
(function () {
|
||||
function getTableSortState(table) {
|
||||
if (!table._tableSortState) {
|
||||
table._tableSortState = {
|
||||
sortKey: '',
|
||||
direction: 'asc'
|
||||
};
|
||||
}
|
||||
return table._tableSortState;
|
||||
}
|
||||
|
||||
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 updateSortableHeaderState(table) {
|
||||
var state = getTableSortState(table);
|
||||
var currentUrl = new URL(window.location.href);
|
||||
var activeSortKey = String(currentUrl.searchParams.get('sort') || '').trim();
|
||||
var activeDirection = String(currentUrl.searchParams.get('direction') || '').trim().toLowerCase() === 'desc' ? 'desc' : 'asc';
|
||||
var headerCells = table.tHead && table.tHead.rows && table.tHead.rows.length ? Array.prototype.slice.call(table.tHead.rows[0].cells || []) : [];
|
||||
|
||||
state.sortKey = activeSortKey;
|
||||
state.direction = activeDirection;
|
||||
|
||||
headerCells.forEach(function (headerCell) {
|
||||
if (!headerCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sortKey = String(headerCell.getAttribute && headerCell.getAttribute('data-table-sort-key') || '').trim();
|
||||
var sortable = Boolean(sortKey) && String(headerCell.getAttribute('data-sortable') || '').toLowerCase() !== 'false';
|
||||
|
||||
headerCell.classList.remove('sort-asc', 'sort-desc', 'sortable', 'unsortable');
|
||||
headerCell.removeAttribute('aria-sort');
|
||||
|
||||
if (!sortable) {
|
||||
headerCell.classList.add('unsortable');
|
||||
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';
|
||||
headerCell.classList.add('sortable');
|
||||
headerCell.setAttribute('role', 'button');
|
||||
headerCell.setAttribute('tabindex', '0');
|
||||
|
||||
if (sortKey === activeSortKey) {
|
||||
headerCell.classList.add(activeDirection === 'desc' ? 'sort-desc' : 'sort-asc');
|
||||
headerCell.setAttribute('aria-sort', activeDirection === 'desc' ? 'descending' : 'ascending');
|
||||
if (activeDirection === 'desc') {
|
||||
indicator.classList.add('bi-caret-down-fill');
|
||||
} else {
|
||||
indicator.classList.add('bi-caret-up-fill');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
headerCell.setAttribute('aria-sort', 'none');
|
||||
indicator.classList.add('bi-arrow-down-up');
|
||||
});
|
||||
}
|
||||
|
||||
function buildSortedUrl(sortKey) {
|
||||
var currentUrl = new URL(window.location.href);
|
||||
var nextUrl = new URL(window.location.href);
|
||||
var currentSortKey = String(currentUrl.searchParams.get('sort') || '').trim();
|
||||
var currentDirection = String(currentUrl.searchParams.get('direction') || '').trim().toLowerCase() === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
if (currentSortKey === sortKey) {
|
||||
nextUrl.searchParams.set('direction', currentDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
nextUrl.searchParams.set('sort', sortKey);
|
||||
nextUrl.searchParams.set('direction', 'asc');
|
||||
}
|
||||
|
||||
nextUrl.searchParams.delete('page');
|
||||
return nextUrl;
|
||||
}
|
||||
|
||||
function updateUrlState(nextUrl) {
|
||||
if (window.history && window.history.replaceState) {
|
||||
window.history.replaceState({}, document.title, nextUrl.pathname + nextUrl.search + nextUrl.hash);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSortedTable(table, nextUrl, requestSequence, sequenceId) {
|
||||
var container = table ? (table.closest('[data-table-search-container]') || table.closest('.card') || null) : null;
|
||||
|
||||
fetch(nextUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/html, application/xhtml+xml',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Unable to load sorted results.');
|
||||
}
|
||||
return response.text();
|
||||
}).then(function (text) {
|
||||
if (sequenceId !== requestSequence.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
||||
updateUrlState(nextUrl);
|
||||
|
||||
if (window.replaceTableResults && replaceTableResults(container, responseDocument)) {
|
||||
if (typeof window.initSortableTables === 'function') {
|
||||
window.initSortableTables(container || document);
|
||||
}
|
||||
if (typeof window.initTableSearches === 'function') {
|
||||
window.initTableSearches(container || document);
|
||||
}
|
||||
if (typeof window.initLocalDateTimes === 'function') {
|
||||
window.initLocalDateTimes(container || document);
|
||||
}
|
||||
}
|
||||
}).catch(function () {
|
||||
window.location.assign(nextUrl.toString());
|
||||
});
|
||||
}
|
||||
|
||||
function initSortableTables(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
|
||||
Array.prototype.forEach.call(scope.querySelectorAll('table[data-table-searchable]'), function (table) {
|
||||
var headerRow = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0] : null;
|
||||
if (!headerRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (table.getAttribute('data-table-sort-bound') === 'true') {
|
||||
updateSortableHeaderState(table);
|
||||
return;
|
||||
}
|
||||
|
||||
table.setAttribute('data-table-sort-bound', 'true');
|
||||
var requestSequence = { value: 0 };
|
||||
|
||||
Array.prototype.forEach.call(headerRow.cells, function (headerCell) {
|
||||
if (!headerCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sortKey = String(headerCell.getAttribute && headerCell.getAttribute('data-table-sort-key') || '').trim();
|
||||
var isSortable = Boolean(sortKey) && String(headerCell.getAttribute('data-sortable') || '').toLowerCase() !== 'false';
|
||||
if (!isSortable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (headerCell.getAttribute('data-table-sort-bound') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
headerCell.setAttribute('data-table-sort-bound', 'true');
|
||||
ensureSortableHeaderIndicator(headerCell);
|
||||
|
||||
headerCell.addEventListener('click', function () {
|
||||
var nextUrl = buildSortedUrl(sortKey);
|
||||
requestSequence.value += 1;
|
||||
var sequenceId = requestSequence.value;
|
||||
refreshSortedTable(table, nextUrl, requestSequence, sequenceId);
|
||||
});
|
||||
|
||||
headerCell.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
headerCell.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateSortableHeaderState(table);
|
||||
});
|
||||
}
|
||||
|
||||
window.initSortableTables = initSortableTables;
|
||||
initSortableTables();
|
||||
}());
|
||||
@@ -1,118 +0,0 @@
|
||||
(function () {
|
||||
var THEME_STORAGE_KEY = 'web-theme';
|
||||
|
||||
function getPreferredTheme() {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function getStoredTheme() {
|
||||
try {
|
||||
var storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (storedTheme === 'dark' || storedTheme === 'light') {
|
||||
return storedTheme;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setStoredTheme(theme) {
|
||||
try {
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
var normalizedTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
var nextTheme = normalizedTheme === 'dark' ? 'light' : 'dark';
|
||||
var nextThemeLabel = nextTheme === 'dark' ? 'Dark mode' : 'Light mode';
|
||||
|
||||
document.documentElement.dataset.bsTheme = normalizedTheme;
|
||||
document.documentElement.style.colorScheme = normalizedTheme;
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-toggle]'), function (toggleButton) {
|
||||
var icon = toggleButton.querySelector('.theme-toggle__icon');
|
||||
toggleButton.setAttribute('aria-pressed', normalizedTheme === 'dark' ? 'true' : 'false');
|
||||
toggleButton.setAttribute('aria-label', 'Switch to ' + nextThemeLabel.toLowerCase());
|
||||
if (icon) {
|
||||
icon.classList.remove('theme-toggle__icon--moon', 'theme-toggle__icon--sun');
|
||||
icon.classList.add(normalizedTheme === 'dark' ? 'theme-toggle__icon--sun' : 'theme-toggle__icon--moon');
|
||||
}
|
||||
});
|
||||
|
||||
return normalizedTheme;
|
||||
}
|
||||
|
||||
function initThemeToggle() {
|
||||
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-theme-toggle]'));
|
||||
var storedTheme = getStoredTheme();
|
||||
var theme = storedTheme || getPreferredTheme();
|
||||
|
||||
applyTheme(theme);
|
||||
|
||||
if (!toggleButtons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.addEventListener('click', function () {
|
||||
var nextTheme = document.documentElement.dataset.bsTheme === 'dark' ? 'light' : 'dark';
|
||||
setStoredTheme(nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle() {
|
||||
var body = document.body;
|
||||
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-sidebar-toggle]'));
|
||||
var backdrop = document.querySelector('[data-sidebar-backdrop]');
|
||||
|
||||
if (!toggleButtons.length || !backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
function setSidebarOpen(isOpen) {
|
||||
body.classList.toggle('is-sidebar-open', isOpen);
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
setSidebarOpen(!body.classList.contains('is-sidebar-open'));
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.addEventListener('click', function () {
|
||||
toggleSidebar();
|
||||
});
|
||||
});
|
||||
|
||||
backdrop.addEventListener('click', function () {
|
||||
setSidebarOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.getPreferredTheme = getPreferredTheme;
|
||||
window.getStoredTheme = getStoredTheme;
|
||||
window.setStoredTheme = setStoredTheme;
|
||||
window.applyTheme = applyTheme;
|
||||
window.initThemeToggle = initThemeToggle;
|
||||
window.initSidebarToggle = initSidebarToggle;
|
||||
|
||||
initThemeToggle();
|
||||
initSidebarToggle();
|
||||
}());
|
||||
Reference in New Issue
Block a user