946 lines
30 KiB
JavaScript
946 lines
30 KiB
JavaScript
(function () {
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function formatDashboardDate(value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
var date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return '';
|
|
}
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
month: 'short',
|
|
day: '2-digit',
|
|
year: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
}).format(date);
|
|
}
|
|
|
|
function getClientRowKey(client) {
|
|
return String(client && client.id ? client.id : client && client.clientId ? client.clientId : '').trim();
|
|
}
|
|
|
|
function getClientDisplayName(client) {
|
|
if (client && client.client_name) {
|
|
return String(client.client_name).trim();
|
|
}
|
|
var clientId = String(client && client.clientId ? client.clientId : '').trim();
|
|
if (clientId) {
|
|
return clientId;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function setButtonVariant(button, classesToRemove, classToAdd) {
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
if (button.classList) {
|
|
classesToRemove.forEach(function (className) {
|
|
button.classList.remove(className);
|
|
});
|
|
if (classToAdd) {
|
|
button.classList.add(classToAdd);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var className = String(button.className || '');
|
|
classesToRemove.forEach(function (removeClass) {
|
|
className = className.replace(new RegExp('(^|\\s)' + removeClass + '(?=\\s|$)', 'g'), ' ');
|
|
});
|
|
if (classToAdd) {
|
|
className += ' ' + classToAdd;
|
|
}
|
|
button.className = className.replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function normalizeDisplayIp(value) {
|
|
var ip = String(value || '').trim();
|
|
if (!ip) {
|
|
return '';
|
|
}
|
|
|
|
if (ip.toLowerCase().indexOf('::ffff:') === 0) {
|
|
return ip.slice(7).trim();
|
|
}
|
|
|
|
return ip;
|
|
}
|
|
|
|
function initConfirmForms() {
|
|
document.addEventListener('submit', function (event) {
|
|
var form = event.target;
|
|
if (!form || !form.getAttribute) {
|
|
return;
|
|
}
|
|
if (form.hasAttribute && form.hasAttribute('data-async-command')) {
|
|
return;
|
|
}
|
|
var message = form.getAttribute('data-confirm-message');
|
|
if (message && !window.confirm(message)) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
|
|
function markFormDirty(form) {
|
|
if (!form || !form.hasAttribute || form.hasAttribute('data-clean-on-load')) {
|
|
return;
|
|
}
|
|
form.dataset.dirty = 'true';
|
|
}
|
|
|
|
function clearFormDirty(form) {
|
|
if (!form) {
|
|
return;
|
|
}
|
|
form.dataset.dirty = 'false';
|
|
}
|
|
|
|
function isFormDirty(form) {
|
|
return Boolean(form && form.dataset && form.dataset.dirty === 'true');
|
|
}
|
|
|
|
function initDirtyTracking() {
|
|
document.addEventListener('input', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.form) {
|
|
return;
|
|
}
|
|
markFormDirty(target.form);
|
|
}, true);
|
|
|
|
document.addEventListener('change', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.form) {
|
|
return;
|
|
}
|
|
markFormDirty(target.form);
|
|
}, true);
|
|
}
|
|
|
|
function initCancelConfirm() {
|
|
document.addEventListener('click', function (event) {
|
|
var cancelTarget = event.target.closest('[data-confirm-unsaved]');
|
|
if (!cancelTarget) {
|
|
return;
|
|
}
|
|
|
|
var form = cancelTarget.form || cancelTarget.closest('form') || document.querySelector('form[data-dirty="true"]');
|
|
if (!isFormDirty(form)) {
|
|
return;
|
|
}
|
|
|
|
var message = cancelTarget.getAttribute('data-confirm-unsaved') || 'You have unsaved changes. Leave this page?';
|
|
if (!window.confirm(message)) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
function initAsyncCommandForms() {
|
|
document.addEventListener('submit', function (event) {
|
|
var form = event.target;
|
|
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-command')) {
|
|
return;
|
|
}
|
|
|
|
if (form.dataset && form.dataset.busy === 'true') {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var message = form.getAttribute('data-confirm-message');
|
|
if (message && !window.confirm(message)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
form.dataset.busy = 'true';
|
|
|
|
var formData = new FormData(form);
|
|
var body = new URLSearchParams();
|
|
formData.forEach(function (value, key) {
|
|
body.append(key, value);
|
|
});
|
|
|
|
fetch(form.action, {
|
|
method: (form.method || 'POST').toUpperCase(),
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
body: body.toString(),
|
|
credentials: 'same-origin'
|
|
}).finally(function () {
|
|
delete form.dataset.busy;
|
|
});
|
|
}, true);
|
|
}
|
|
|
|
function initAsyncSaveForms() {
|
|
var refreshSequence = 0;
|
|
|
|
function setSaveActionValue(form, value) {
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
var hiddenInput = form.querySelector('input[type="hidden"][name="save_action"]');
|
|
if (!hiddenInput) {
|
|
hiddenInput = document.createElement('input');
|
|
hiddenInput.type = 'hidden';
|
|
hiddenInput.name = 'save_action';
|
|
form.appendChild(hiddenInput);
|
|
}
|
|
|
|
hiddenInput.value = String(value || '').trim().toLowerCase();
|
|
}
|
|
|
|
function getResponseQueryValue(responseUrl, key) {
|
|
try {
|
|
var url = new URL(responseUrl, window.location.href);
|
|
return String(url.searchParams.get(key) || '').trim();
|
|
} catch (_error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function rebindRefreshTarget(targetElement) {
|
|
if (!targetElement) {
|
|
return;
|
|
}
|
|
|
|
if (typeof window.initJsonTogglePanels === 'function') {
|
|
window.initJsonTogglePanels(targetElement);
|
|
}
|
|
if (typeof window.initLocalDateTimes === 'function') {
|
|
window.initLocalDateTimes(targetElement);
|
|
}
|
|
if (typeof window.initTableSearches === 'function') {
|
|
window.initTableSearches(targetElement);
|
|
}
|
|
if (typeof window.initTablePaginations === 'function') {
|
|
window.initTablePaginations(targetElement);
|
|
}
|
|
}
|
|
|
|
function replaceRefreshTargetFromPage(refreshTargetSelector, sequenceId) {
|
|
if (sequenceId !== refreshSequence) {
|
|
return;
|
|
}
|
|
|
|
fetch(window.location.href, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'text/html, application/xhtml+xml'
|
|
},
|
|
credentials: 'same-origin',
|
|
cache: 'no-store'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
throw new Error('Unable to refresh saved data.');
|
|
}
|
|
return response.text();
|
|
}).then(function (text) {
|
|
if (sequenceId !== refreshSequence) {
|
|
return;
|
|
}
|
|
|
|
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
|
var currentTarget = document.querySelector(refreshTargetSelector);
|
|
var nextTarget = responseDocument.querySelector(refreshTargetSelector);
|
|
if (!currentTarget || !nextTarget) {
|
|
return;
|
|
}
|
|
|
|
currentTarget.outerHTML = nextTarget.outerHTML;
|
|
rebindRefreshTarget(document.querySelector(refreshTargetSelector));
|
|
}).catch(function (_error) {
|
|
// Ignore refresh replacement failures and leave the existing content in place.
|
|
});
|
|
}
|
|
|
|
function watchRefreshTask(refreshStateUrl, refreshTaskId, refreshTargetSelector, sequenceId) {
|
|
var pollDelayMs = 1000;
|
|
var maxAttempts = 60;
|
|
|
|
function poll(attempt) {
|
|
if (sequenceId !== refreshSequence) {
|
|
return;
|
|
}
|
|
|
|
var stateUrl;
|
|
try {
|
|
stateUrl = new URL(refreshStateUrl, window.location.href);
|
|
} catch (_error) {
|
|
return;
|
|
}
|
|
stateUrl.searchParams.set('refresh_task_id', refreshTaskId);
|
|
|
|
fetch(stateUrl.toString(), {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
credentials: 'same-origin',
|
|
cache: 'no-store'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
throw new Error('Unable to check refresh status.');
|
|
}
|
|
return response.json();
|
|
}).then(function (payload) {
|
|
if (sequenceId !== refreshSequence) {
|
|
return;
|
|
}
|
|
|
|
var status = String(payload && payload.status || '').trim().toLowerCase();
|
|
if (status === 'queued' || status === 'running') {
|
|
if (attempt < maxAttempts) {
|
|
window.setTimeout(function () {
|
|
poll(attempt + 1);
|
|
}, pollDelayMs);
|
|
}
|
|
return;
|
|
}
|
|
|
|
replaceRefreshTargetFromPage(refreshTargetSelector, sequenceId);
|
|
}).catch(function () {
|
|
if (attempt < maxAttempts) {
|
|
window.setTimeout(function () {
|
|
poll(attempt + 1);
|
|
}, pollDelayMs);
|
|
}
|
|
});
|
|
}
|
|
|
|
poll(0);
|
|
}
|
|
|
|
document.addEventListener('click', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.closest) {
|
|
return;
|
|
}
|
|
|
|
var button = target.closest('button[name="save_action"]');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
var form = button.form || button.closest('form');
|
|
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-save')) {
|
|
return;
|
|
}
|
|
|
|
setSaveActionValue(form, button.value || '');
|
|
form.dataset.submitterValue = String(button.value || '').trim().toLowerCase();
|
|
}, true);
|
|
|
|
document.addEventListener('submit', function (event) {
|
|
var form = event.target;
|
|
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-save')) {
|
|
return;
|
|
}
|
|
|
|
if (form.dataset && form.dataset.busy === 'true') {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var message = form.getAttribute('data-confirm-message');
|
|
if (message && !window.confirm(message)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
form.dataset.busy = 'true';
|
|
|
|
var hiddenSaveAction = form.querySelector('input[type="hidden"][name="save_action"]');
|
|
var formData = new FormData(form);
|
|
var submitterValue = String((hiddenSaveAction && hiddenSaveAction.value) || form.dataset.submitterValue || '').trim().toLowerCase();
|
|
if (event.submitter && event.submitter.name) {
|
|
submitterValue = String(event.submitter.value || '').trim().toLowerCase();
|
|
formData.set(event.submitter.name, event.submitter.value || '');
|
|
}
|
|
var hasFileValue = false;
|
|
formData.forEach(function (value) {
|
|
if (value && typeof value === 'object' && typeof value.name === 'string') {
|
|
hasFileValue = true;
|
|
}
|
|
});
|
|
|
|
var isMultipart = hasFileValue || String(form.enctype || '').toLowerCase() === 'multipart/form-data';
|
|
var body = isMultipart ? formData : new URLSearchParams();
|
|
|
|
if (!isMultipart) {
|
|
formData.forEach(function (value, key) {
|
|
body.append(key, value);
|
|
});
|
|
}
|
|
|
|
fetch(form.action, {
|
|
method: (form.method || 'POST').toUpperCase(),
|
|
headers: Object.assign({
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'text/html, application/json, text/plain, */*'
|
|
}, isMultipart ? {} : {
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
}),
|
|
body: isMultipart ? body : body.toString(),
|
|
credentials: 'same-origin'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
return response.text().then(function (text) {
|
|
var error = new Error(text || 'Unable to save changes.');
|
|
error.status = response.status;
|
|
throw error;
|
|
});
|
|
}
|
|
var actionUrl = '';
|
|
try {
|
|
actionUrl = new URL(form.action, window.location.href).pathname;
|
|
} catch (_error) {
|
|
actionUrl = String(form.action || '');
|
|
}
|
|
var refreshTargetSelector = String(form.getAttribute('data-async-save-refresh-target') || '').trim();
|
|
var refreshStateUrl = String(form.getAttribute('data-async-save-refresh-state-url') || '').trim();
|
|
var refreshTaskId = getResponseQueryValue(response.url || '', 'refresh_task_id');
|
|
var shouldFollowRedirect = Boolean(form.hasAttribute('data-async-save-new-url')) && !/\/\d+(?:\/|$)/.test(actionUrl);
|
|
if (submitterValue === 'close' || submitterValue === 'new') {
|
|
var redirectUrl = submitterValue === 'close'
|
|
? String(form.dataset.asyncSaveCloseUrl || response.url || window.location.href)
|
|
: String(form.dataset.asyncSaveNewUrl || response.url || window.location.href);
|
|
window.location.replace(redirectUrl);
|
|
return;
|
|
}
|
|
if (shouldFollowRedirect && response.url) {
|
|
clearFormDirty(form);
|
|
window.location.replace(response.url);
|
|
return;
|
|
}
|
|
if (form.hasAttribute('data-async-save-reload-on-success')) {
|
|
clearFormDirty(form);
|
|
window.location.replace(response.url || window.location.href);
|
|
return;
|
|
}
|
|
clearFormDirty(form);
|
|
return response.text().then(function (text) {
|
|
var savedMessage = '';
|
|
var responseDocument = null;
|
|
try {
|
|
responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
|
var toastBody = responseDocument.querySelector('.toast-body');
|
|
if (toastBody && toastBody.textContent) {
|
|
savedMessage = toastBody.textContent.trim();
|
|
}
|
|
} catch (_error) {
|
|
savedMessage = '';
|
|
}
|
|
|
|
if (refreshTargetSelector && refreshStateUrl && refreshTaskId) {
|
|
refreshSequence += 1;
|
|
watchRefreshTask(refreshStateUrl, refreshTaskId, refreshTargetSelector, refreshSequence);
|
|
} else if (refreshTargetSelector && responseDocument) {
|
|
var currentTarget = document.querySelector(refreshTargetSelector);
|
|
var nextTarget = responseDocument.querySelector(refreshTargetSelector);
|
|
if (currentTarget && nextTarget) {
|
|
currentTarget.outerHTML = nextTarget.outerHTML;
|
|
rebindRefreshTarget(document.querySelector(refreshTargetSelector));
|
|
}
|
|
}
|
|
|
|
showToast(savedMessage || 'Saved.', 'success');
|
|
});
|
|
}).catch(function (error) {
|
|
if (typeof showToast === 'function') {
|
|
var variant = Number(error && error.status) >= 400 && Number(error && error.status) < 500 ? 'warning' : 'danger';
|
|
showToast(error.message || 'Unable to save changes.', variant);
|
|
return;
|
|
}
|
|
window.alert(error.message || 'Unable to save changes.');
|
|
}).finally(function () {
|
|
delete form.dataset.busy;
|
|
delete form.dataset.submitterValue;
|
|
if (hiddenSaveAction) {
|
|
hiddenSaveAction.value = '';
|
|
}
|
|
});
|
|
}, true);
|
|
}
|
|
|
|
function initSubmitOnChange() {
|
|
var fields = document.querySelectorAll('[data-submit-on-change]');
|
|
Array.prototype.forEach.call(fields, function (field) {
|
|
field.addEventListener('change', function () {
|
|
var form = field.form || field.closest('form');
|
|
if (form) {
|
|
form.submit();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function initJsonTogglePanels(root) {
|
|
var scope = root && root.querySelectorAll ? root : document;
|
|
var panels = scope.querySelectorAll('[data-json-toggle-panel]');
|
|
Array.prototype.forEach.call(panels, function (panel) {
|
|
var output = panel.querySelector('[data-json-toggle-output]');
|
|
if (!output) {
|
|
return;
|
|
}
|
|
|
|
var card = panel.closest ? panel.closest('.card') : null;
|
|
var button = card ? card.querySelector('[data-json-toggle]') : null;
|
|
var label = button ? button.querySelector('[data-json-toggle-label]') : null;
|
|
var sourceNode = panel.querySelector('[data-json-toggle-source]');
|
|
var rawJson = '';
|
|
try {
|
|
rawJson = JSON.parse(String(sourceNode ? sourceNode.textContent : '""'));
|
|
} catch (_error) {
|
|
rawJson = '';
|
|
}
|
|
|
|
if (typeof rawJson !== 'string' || !rawJson.trim()) {
|
|
if (button) {
|
|
button.classList.add('d-none');
|
|
}
|
|
return;
|
|
}
|
|
|
|
var parsedJson;
|
|
try {
|
|
parsedJson = JSON.parse(rawJson);
|
|
} catch (_error) {
|
|
if (button) {
|
|
button.classList.add('d-none');
|
|
}
|
|
return;
|
|
}
|
|
|
|
var compactJson = JSON.stringify(parsedJson);
|
|
var formattedJson = JSON.stringify(parsedJson, null, 2);
|
|
var isFormatted = true;
|
|
|
|
function syncButtonLabel() {
|
|
if (!button || !label) {
|
|
return;
|
|
}
|
|
label.textContent = isFormatted
|
|
? String(button.getAttribute('data-json-toggle-label-compact') || 'Unformat JSON')
|
|
: String(button.getAttribute('data-json-toggle-label-formatted') || 'Format JSON');
|
|
button.setAttribute('aria-pressed', isFormatted ? 'true' : 'false');
|
|
}
|
|
|
|
function syncOutput() {
|
|
output.textContent = isFormatted ? formattedJson : compactJson;
|
|
syncButtonLabel();
|
|
}
|
|
|
|
output.textContent = formattedJson;
|
|
syncButtonLabel();
|
|
|
|
if (button) {
|
|
button.addEventListener('click', function () {
|
|
isFormatted = !isFormatted;
|
|
syncOutput();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function initLocalDateTimes(root) {
|
|
var scope = root && root.querySelectorAll ? root : document;
|
|
var elements = scope.querySelectorAll('[data-local-datetime]');
|
|
Array.prototype.forEach.call(elements, function (element) {
|
|
var rawValue = String(element.getAttribute('datetime') || element.getAttribute('data-local-datetime') || element.textContent || '').trim();
|
|
if (!rawValue) {
|
|
return;
|
|
}
|
|
|
|
var date = new Date(rawValue);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return;
|
|
}
|
|
|
|
element.textContent = formatDashboardDate(date);
|
|
});
|
|
}
|
|
|
|
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();
|
|
});
|
|
}
|
|
|
|
function createSlideThumbPlaceholder() {
|
|
var placeholder = document.createElement('span');
|
|
var icon = document.createElement('i');
|
|
|
|
placeholder.className = 'playlist-slide-thumb-placeholder';
|
|
icon.className = 'bi bi-image';
|
|
icon.setAttribute('aria-hidden', 'true');
|
|
placeholder.appendChild(icon);
|
|
return placeholder;
|
|
}
|
|
|
|
function attachSlideThumbFallbacks(root) {
|
|
var scope = root && root.querySelectorAll ? root : document;
|
|
|
|
Array.prototype.forEach.call(scope.querySelectorAll('.playlist-slide-thumb-image'), function (image) {
|
|
if (image.getAttribute('data-slide-thumb-fallback-bound') === 'true') {
|
|
return;
|
|
}
|
|
image.setAttribute('data-slide-thumb-fallback-bound', 'true');
|
|
image.addEventListener('error', function () {
|
|
if (image.parentNode) {
|
|
image.parentNode.replaceChild(createSlideThumbPlaceholder(), image);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (window.initSortableTables) {
|
|
window.initSortableTables();
|
|
}
|
|
|
|
initConfirmForms();
|
|
initDirtyTracking();
|
|
initCancelConfirm();
|
|
initAsyncCommandForms();
|
|
initAsyncSaveForms();
|
|
initSubmitOnChange();
|
|
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;
|
|
}());
|