563 lines
18 KiB
JavaScript
563 lines
18 KiB
JavaScript
(function () {
|
|
var webUiHelpers = window.webUiHelpers || {};
|
|
var escapeHtml = webUiHelpers.escapeHtml;
|
|
var formatDashboardDate = webUiHelpers.formatDashboardDate;
|
|
var getClientRowKey = webUiHelpers.getClientRowKey;
|
|
var getClientDisplayName = webUiHelpers.getClientDisplayName;
|
|
var setButtonVariant = webUiHelpers.setButtonVariant;
|
|
var normalizeDisplayIp = webUiHelpers.normalizeDisplayIp;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 () {
|
|
// 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 initTableSearches(root) {
|
|
if (typeof window.initTableSearches === 'function') {
|
|
window.initTableSearches(root);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
initConfirmForms();
|
|
initDirtyTracking();
|
|
initCancelConfirm();
|
|
initAsyncCommandForms();
|
|
initAsyncSaveForms();
|
|
initSubmitOnChange();
|
|
initJsonTogglePanels();
|
|
initLocalDateTimes();
|
|
initTableSearches();
|
|
attachSlideThumbFallbacks(document);
|
|
window.initJsonTogglePanels = initJsonTogglePanels;
|
|
window.initLocalDateTimes = initLocalDateTimes;
|
|
window.createSlideThumbPlaceholder = createSlideThumbPlaceholder;
|
|
window.attachSlideThumbFallbacks = attachSlideThumbFallbacks;
|
|
}());
|