1058 lines
34 KiB
JavaScript
1058 lines
34 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();
|
|
return;
|
|
}
|
|
|
|
var actionPath = '';
|
|
try {
|
|
actionPath = new URL(form.action, window.location.href).pathname;
|
|
} catch (_error) {
|
|
actionPath = String(form.action || '');
|
|
}
|
|
|
|
if (!/\/delete(?:\/|$)/.test(actionPath)) {
|
|
return;
|
|
}
|
|
|
|
if (form.dataset && form.dataset.busy === 'true') {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
form.dataset.busy = 'true';
|
|
|
|
fetch(form.action, {
|
|
method: (form.method || 'POST').toUpperCase(),
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
credentials: 'same-origin'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
return response.text().then(function (text) {
|
|
var error = new Error(text || 'Unable to delete item.');
|
|
error.status = response.status;
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
if (isLoginRedirect(response)) {
|
|
window.location.replace(response.url || '/login');
|
|
return;
|
|
}
|
|
|
|
window.location.replace(response.url || window.location.href);
|
|
}).catch(function (error) {
|
|
if (typeof showToast === 'function') {
|
|
var status = Number(error && error.status);
|
|
var variant = status === 403 ? 'warning' : status >= 400 && status < 500 ? 'warning' : 'danger';
|
|
showToast(error.message || 'Unable to delete item.', variant);
|
|
return;
|
|
}
|
|
window.alert(error.message || 'Unable to delete item.');
|
|
}).finally(function () {
|
|
delete form.dataset.busy;
|
|
});
|
|
});
|
|
}
|
|
|
|
function initDeleteButtons() {
|
|
document.addEventListener('click', function (event) {
|
|
var deleteButton = event.target.closest('[data-delete-action-url]');
|
|
if (!deleteButton) {
|
|
return;
|
|
}
|
|
|
|
if (deleteButton.disabled || (deleteButton.dataset && deleteButton.dataset.busy === 'true')) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var actionUrl = deleteButton.getAttribute('data-delete-action-url');
|
|
if (!actionUrl) {
|
|
return;
|
|
}
|
|
|
|
var message = deleteButton.getAttribute('data-confirm-message');
|
|
if (message && !window.confirm(message)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
deleteButton.dataset.busy = 'true';
|
|
|
|
fetch(actionUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
credentials: 'same-origin'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
return response.text().then(function (text) {
|
|
var error = new Error(text || 'Unable to delete item.');
|
|
error.status = response.status;
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
if (isLoginRedirect(response)) {
|
|
window.location.replace(response.url || '/login');
|
|
return;
|
|
}
|
|
|
|
window.location.replace(response.url || window.location.href);
|
|
}).catch(function (error) {
|
|
if (typeof showToast === 'function') {
|
|
var status = Number(error && error.status);
|
|
var variant = status === 403 ? 'warning' : status >= 400 && status < 500 ? 'warning' : 'danger';
|
|
showToast(error.message || 'Unable to delete item.', variant);
|
|
return;
|
|
}
|
|
window.alert(error.message || 'Unable to delete item.');
|
|
}).finally(function () {
|
|
delete deleteButton.dataset.busy;
|
|
});
|
|
});
|
|
}
|
|
|
|
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 isRegionEditorField(target) {
|
|
return Boolean(target && typeof target.closest === 'function' && target.closest('.region-item'));
|
|
}
|
|
|
|
function initDirtyTracking() {
|
|
document.addEventListener('input', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.form || isRegionEditorField(target)) {
|
|
return;
|
|
}
|
|
markFormDirty(target.form);
|
|
}, true);
|
|
|
|
document.addEventListener('change', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.form || isRegionEditorField(target)) {
|
|
return;
|
|
}
|
|
markFormDirty(target.form);
|
|
}, true);
|
|
}
|
|
|
|
function clampLimitedInputValue(input) {
|
|
if (!input || !input.matches || !input.matches('input[data-limit-input-value]')) {
|
|
return;
|
|
}
|
|
|
|
var minAttr = input.getAttribute('min');
|
|
var maxAttr = input.getAttribute('max');
|
|
var minValue = minAttr === null || minAttr === '' ? null : Number(minAttr);
|
|
var maxValue = maxAttr === null || maxAttr === '' ? null : Number(maxAttr);
|
|
var rawValue = String(input.value || '').trim();
|
|
|
|
if (!rawValue) {
|
|
return;
|
|
}
|
|
|
|
var numericValue = Number(rawValue);
|
|
if (!Number.isFinite(numericValue)) {
|
|
return;
|
|
}
|
|
|
|
if (Number.isFinite(minValue) && numericValue < minValue) {
|
|
input.value = String(minValue);
|
|
return;
|
|
}
|
|
|
|
if (Number.isFinite(maxValue) && numericValue > maxValue) {
|
|
input.value = String(maxValue);
|
|
}
|
|
}
|
|
|
|
function initLimitedInputValues() {
|
|
document.addEventListener('input', function (event) {
|
|
clampLimitedInputValue(event.target);
|
|
}, true);
|
|
|
|
document.addEventListener('change', function (event) {
|
|
clampLimitedInputValue(event.target);
|
|
}, true);
|
|
|
|
document.addEventListener('paste', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.matches || !target.matches('input[data-limit-input-value]')) {
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(function () {
|
|
clampLimitedInputValue(target);
|
|
}, 0);
|
|
}, true);
|
|
}
|
|
|
|
function clampLimitedTextValue(input) {
|
|
if (!input || !input.matches || !input.matches('input[data-limit-text-length], textarea[data-limit-text-length]')) {
|
|
return;
|
|
}
|
|
|
|
var maxLength = Number(input.getAttribute('maxlength'));
|
|
if (!Number.isFinite(maxLength) || maxLength < 1) {
|
|
return;
|
|
}
|
|
|
|
var value = String(input.value || '');
|
|
if (value.length > maxLength) {
|
|
input.value = value.slice(0, maxLength);
|
|
}
|
|
}
|
|
|
|
function initLimitedTextInputValues() {
|
|
document.addEventListener('input', function (event) {
|
|
clampLimitedTextValue(event.target);
|
|
}, true);
|
|
|
|
document.addEventListener('change', function (event) {
|
|
clampLimitedTextValue(event.target);
|
|
}, true);
|
|
|
|
document.addEventListener('paste', function (event) {
|
|
var target = event.target;
|
|
if (!target || !target.matches || !target.matches('input[data-limit-text-length], textarea[data-limit-text-length]')) {
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(function () {
|
|
clampLimitedTextValue(target);
|
|
}, 0);
|
|
}, 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 parseAsyncSaveResponseMessage(responseText) {
|
|
var savedMessage = '';
|
|
try {
|
|
var responseDocument = new DOMParser().parseFromString(responseText || '', 'text/html');
|
|
var toastBody = responseDocument.querySelector('.toast-body');
|
|
if (toastBody && toastBody.textContent) {
|
|
savedMessage = toastBody.textContent.trim();
|
|
}
|
|
} catch (_error) {
|
|
savedMessage = '';
|
|
}
|
|
|
|
return savedMessage;
|
|
}
|
|
|
|
function parseAsyncSaveErrorMessage(responseText) {
|
|
var errorMessage = parseAsyncSaveResponseMessage(responseText);
|
|
if (errorMessage) {
|
|
return errorMessage;
|
|
}
|
|
|
|
return String(responseText || '').trim();
|
|
}
|
|
|
|
function isWarningSaveError(error) {
|
|
var status = Number(error && error.status);
|
|
if (status === 400 || status === 422) {
|
|
return true;
|
|
}
|
|
|
|
return /region names must be unique/i.test(String(error && error.message || ''));
|
|
}
|
|
|
|
function getAsyncSaveActionField(form) {
|
|
if (!form || !form.querySelector) {
|
|
return null;
|
|
}
|
|
|
|
return form.querySelector('input[type="hidden"][name="save_action"]') || form.querySelector('input[type="hidden"][name="action"]');
|
|
}
|
|
|
|
function isLoginRedirect(response) {
|
|
if (!response || !response.redirected || !response.url) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return new URL(response.url, window.location.href).pathname === '/login';
|
|
} catch (_error) {
|
|
return /\/login(?:[?#]|$)/.test(String(response.url || ''));
|
|
}
|
|
}
|
|
|
|
async function submitAsyncSaveForm(form, event, options) {
|
|
var settings = options || {};
|
|
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-save')) {
|
|
return false;
|
|
}
|
|
|
|
if (form.dataset && form.dataset.busy === 'true') {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var message = form.getAttribute('data-confirm-message');
|
|
if (message && !window.confirm(message)) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
form.dataset.busy = 'true';
|
|
|
|
try {
|
|
if (typeof settings.beforeSubmit === 'function') {
|
|
var beforeResult = await settings.beforeSubmit(form, event);
|
|
if (beforeResult === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var hiddenSaveAction = getAsyncSaveActionField(form);
|
|
var formData = new FormData(form);
|
|
var submitterValue = String((hiddenSaveAction && hiddenSaveAction.value) || form.dataset.submitterValue || '').trim().toLowerCase();
|
|
if (event && 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);
|
|
});
|
|
}
|
|
|
|
var response = await 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'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
var responseText = await response.text();
|
|
var error = new Error(parseAsyncSaveErrorMessage(responseText) || (settings.errorMessage || 'Unable to save changes.'));
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
|
|
if (isLoginRedirect(response)) {
|
|
window.location.replace(response.url || '/login');
|
|
return true;
|
|
}
|
|
|
|
var responseText = await response.text();
|
|
var responseDocument = null;
|
|
try {
|
|
responseDocument = new DOMParser().parseFromString(responseText || '', 'text/html');
|
|
} catch (_error) {
|
|
responseDocument = null;
|
|
}
|
|
|
|
if (typeof settings.afterSuccess === 'function') {
|
|
await settings.afterSuccess({
|
|
form: form,
|
|
response: response,
|
|
responseText: responseText,
|
|
responseDocument: responseDocument,
|
|
submitterValue: submitterValue
|
|
});
|
|
}
|
|
|
|
try {
|
|
form.dispatchEvent(new CustomEvent('web-async-save:success', {
|
|
bubbles: true,
|
|
detail: {
|
|
form: form,
|
|
response: response,
|
|
responseText: responseText,
|
|
responseDocument: responseDocument,
|
|
submitterValue: submitterValue
|
|
}
|
|
}));
|
|
} catch (_error) {
|
|
// Ignore event dispatch failures and continue the save flow.
|
|
}
|
|
|
|
clearFormDirty(form);
|
|
|
|
if (submitterValue === 'close' || submitterValue === 'new') {
|
|
var redirectUrl = submitterValue === 'close'
|
|
? String(response.url || form.dataset.asyncSaveCloseUrl || window.location.href)
|
|
: String(response.url || form.dataset.asyncSaveNewUrl || window.location.href);
|
|
window.location.replace(redirectUrl);
|
|
return true;
|
|
}
|
|
|
|
if (form.dataset && form.dataset.asyncSaveNewRedirect === 'response-url') {
|
|
window.location.replace(String(response.url || form.dataset.asyncSaveNewUrl || window.location.href));
|
|
return true;
|
|
}
|
|
|
|
var successMessage = typeof settings.getSuccessMessage === 'function'
|
|
? settings.getSuccessMessage({
|
|
form: form,
|
|
response: response,
|
|
responseText: responseText,
|
|
responseDocument: responseDocument,
|
|
submitterValue: submitterValue
|
|
})
|
|
: parseAsyncSaveResponseMessage(responseText);
|
|
|
|
var resolvedSuccessMessage = successMessage || String(settings.fallbackSuccessMessage || 'Saved.');
|
|
|
|
if (form.dataset && form.dataset.asyncSaveRefreshPage === 'true') {
|
|
if (typeof window.queueToastAfterRefresh === 'function') {
|
|
window.queueToastAfterRefresh(resolvedSuccessMessage, 'success');
|
|
} else if (typeof showToast === 'function') {
|
|
showToast(resolvedSuccessMessage, 'success');
|
|
}
|
|
window.setTimeout(function () {
|
|
window.location.reload();
|
|
}, 1000);
|
|
return true;
|
|
}
|
|
|
|
showToast(resolvedSuccessMessage, 'success');
|
|
return true;
|
|
} catch (error) {
|
|
if (typeof showToast === 'function') {
|
|
var variant = isWarningSaveError(error) ? 'warning' : 'danger';
|
|
showToast(error && error.message ? error.message : (settings.errorMessage || 'Unable to save changes.'), variant);
|
|
} else {
|
|
window.alert(error && error.message ? error.message : (settings.errorMessage || 'Unable to save changes.'));
|
|
}
|
|
return false;
|
|
} finally {
|
|
delete form.dataset.busy;
|
|
delete form.dataset.submitterValue;
|
|
if (typeof settings.afterFinally === 'function') {
|
|
settings.afterFinally(form);
|
|
}
|
|
var hiddenInput = getAsyncSaveActionField(form);
|
|
if (hiddenInput) {
|
|
hiddenInput.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
window.webAsyncSaveForm = window.webAsyncSaveForm || {};
|
|
window.webAsyncSaveForm.submit = submitAsyncSaveForm;
|
|
window.webAsyncSaveForm.parseResponseMessage = parseAsyncSaveResponseMessage;
|
|
|
|
function initAsyncCommandForms() {
|
|
function getAnnouncementActionState(actionPath) {
|
|
var isPlay = /\/play$/.test(actionPath);
|
|
return {
|
|
nextActionPath: actionPath.replace(/\/(?:play|stop)$/, isPlay ? '/stop' : '/play'),
|
|
nextLabel: isPlay ? 'Stop' : 'Play',
|
|
nextIcon: isPlay ? 'bi-stop-fill' : 'bi-play-fill',
|
|
nextClassName: isPlay ? 'btn-outline-warning' : 'btn-outline-success',
|
|
nextConfirmMessage: isPlay
|
|
? 'Stop this announcement on the selected screens now?'
|
|
: 'Send this announcement to the selected screens now?'
|
|
};
|
|
}
|
|
|
|
function updateAnnouncementActionButton(form, actionPath) {
|
|
if (!form) {
|
|
return false;
|
|
}
|
|
|
|
var state = getAnnouncementActionState(actionPath);
|
|
var visibleButton = form.querySelector('button[type="submit"]') || (form.id ? document.querySelector('button[form="' + form.id + '"]') : null);
|
|
if (!visibleButton) {
|
|
return false;
|
|
}
|
|
|
|
form.action = state.nextActionPath;
|
|
form.setAttribute('data-confirm-message', state.nextConfirmMessage);
|
|
visibleButton.className = visibleButton.className.replace(/btn-outline-(success|warning)/g, state.nextClassName);
|
|
visibleButton.innerHTML = '<i class="bi ' + state.nextIcon + ' me-1" aria-hidden="true"></i>' + state.nextLabel;
|
|
return true;
|
|
}
|
|
|
|
function updateFontToggleRow(form) {
|
|
if (!form) {
|
|
return false;
|
|
}
|
|
|
|
var row = form.closest ? form.closest('tr[data-font-toggle-row]') : null;
|
|
if (!row) {
|
|
return false;
|
|
}
|
|
|
|
var enabledInput = form.querySelector('input[name="enabled"]');
|
|
var toggleButton = form.querySelector('button[type="submit"]');
|
|
var statusBadge = row.querySelector('[data-font-status-badge]');
|
|
var isCurrentlyEnabled = String(row.getAttribute('data-font-enabled') || '').trim() === 'true';
|
|
var willEnable = !isCurrentlyEnabled;
|
|
|
|
if (enabledInput) {
|
|
enabledInput.value = isCurrentlyEnabled ? '0' : '1';
|
|
}
|
|
|
|
if (toggleButton) {
|
|
toggleButton.textContent = willEnable ? 'Disable' : 'Enable';
|
|
}
|
|
|
|
if (statusBadge) {
|
|
statusBadge.className = statusBadge.className.replace(/text-bg-(success|secondary)/g, willEnable ? 'text-bg-success' : 'text-bg-secondary');
|
|
statusBadge.textContent = willEnable ? 'Enabled' : 'Disabled';
|
|
}
|
|
|
|
row.setAttribute('data-font-enabled', willEnable ? 'true' : 'false');
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
var actionPath = '';
|
|
try {
|
|
actionPath = new URL(form.action, window.location.href).pathname;
|
|
} catch (_error) {
|
|
actionPath = String(form.action || '');
|
|
}
|
|
var shouldReloadAfterSuccess = /^\/announcements\/\d+\/(?:play|stop)$/.test(actionPath);
|
|
|
|
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'
|
|
}).then(function (response) {
|
|
if (!response || Number(response.status) >= 400) {
|
|
return;
|
|
}
|
|
|
|
if (/^\/settings\/fonts\/[^/]+\/toggle$/.test(actionPath)) {
|
|
updateFontToggleRow(form);
|
|
return;
|
|
}
|
|
|
|
if (shouldReloadAfterSuccess && response && response.ok) {
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
}).finally(function () {
|
|
delete form.dataset.busy;
|
|
});
|
|
}, true);
|
|
}
|
|
|
|
function initAsyncSaveForms() {
|
|
var refreshSequence = 0;
|
|
|
|
function setSaveActionValue(form, value) {
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
var hiddenInput = getAsyncSaveActionField(form);
|
|
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 (targetElement.id === 'playlist-items-body' && typeof window.rebindPlaylistTableDrag === 'function') {
|
|
window.rebindPlaylistTableDrag(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"], button[name="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;
|
|
}
|
|
submitAsyncSaveForm(form, event, {
|
|
fallbackSuccessMessage: 'Saved.',
|
|
afterSuccess: function (context) {
|
|
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(context.response.url || '', 'refresh_task_id');
|
|
|
|
if (refreshTargetSelector && refreshStateUrl && refreshTaskId) {
|
|
refreshSequence += 1;
|
|
watchRefreshTask(refreshStateUrl, refreshTaskId, refreshTargetSelector, refreshSequence);
|
|
} else if (refreshTargetSelector && context.responseDocument) {
|
|
var currentTarget = document.querySelector(refreshTargetSelector);
|
|
var nextTarget = context.responseDocument.querySelector(refreshTargetSelector);
|
|
if (currentTarget && nextTarget) {
|
|
currentTarget.outerHTML = nextTarget.outerHTML;
|
|
rebindRefreshTarget(document.querySelector(refreshTargetSelector));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, 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 renderJsonPrimitive(value) {
|
|
if (value === null) {
|
|
return '<span class="text-body-secondary">null</span>';
|
|
}
|
|
if (value === true || value === false) {
|
|
return '<span class="text-success">' + String(value) + '</span>';
|
|
}
|
|
if (typeof value === 'number') {
|
|
return '<span class="text-info">' + escapeHtml(String(value)) + '</span>';
|
|
}
|
|
return '<span class="text-body">"' + escapeHtml(String(value)) + '"</span>';
|
|
}
|
|
|
|
function renderJsonNode(key, value, isRoot) {
|
|
var isArray = Array.isArray(value);
|
|
var isObject = value && typeof value === 'object' && !isArray;
|
|
|
|
if (!isArray && !isObject) {
|
|
var primitiveLabel = isRoot ? 'value' : String(key);
|
|
return '<div class="json-tree-leaf"><span class="json-tree-key">' + escapeHtml(primitiveLabel) + '</span><span class="mx-1">:</span>' + renderJsonPrimitive(value) + '</div>';
|
|
}
|
|
|
|
var entries = isArray
|
|
? value.map(function (entry, index) {
|
|
return renderJsonNode('[' + index + ']', entry, false);
|
|
}).join('')
|
|
: Object.keys(value).map(function (childKey) {
|
|
return renderJsonNode(childKey, value[childKey], false);
|
|
}).join('');
|
|
|
|
var summaryLabel = isRoot ? (isArray ? 'Array' : 'Object') : String(key);
|
|
var summaryMeta = isArray ? '[' + value.length + ']' : '{' + Object.keys(value).length + '}';
|
|
return '' +
|
|
'<details class="json-tree-node" open>' +
|
|
'<summary><span class="json-tree-key">' + escapeHtml(summaryLabel) + '</span><span class="mx-1">:</span><span class="text-body-secondary">' + escapeHtml(summaryMeta) + '</span></summary>' +
|
|
'<div class="ms-3 ps-3 border-start">' + entries + '</div>' +
|
|
'</details>';
|
|
}
|
|
|
|
function renderJsonTree(value) {
|
|
if (Array.isArray(value) || (value && typeof value === 'object')) {
|
|
return renderJsonNode('', value, true);
|
|
}
|
|
return renderJsonNode('value', value, true);
|
|
}
|
|
|
|
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 collapseButton = card ? card.querySelector('[data-json-toggle-collapse-all]') : null;
|
|
var expandButton = card ? card.querySelector('[data-json-toggle-expand-all]') : 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 formattedJson = JSON.stringify(parsedJson, null, 2);
|
|
var formattedTree = renderJsonTree(parsedJson);
|
|
|
|
function setAllSectionsExpanded(expanded) {
|
|
var details = output.querySelectorAll('details');
|
|
Array.prototype.forEach.call(details, function (detail) {
|
|
detail.open = expanded;
|
|
});
|
|
|
|
if (collapseButton) {
|
|
collapseButton.disabled = !details.length || !expanded;
|
|
}
|
|
if (expandButton) {
|
|
expandButton.disabled = !details.length || expanded;
|
|
}
|
|
}
|
|
|
|
function syncButtons() {
|
|
if (collapseButton) {
|
|
collapseButton.setAttribute('aria-pressed', 'false');
|
|
}
|
|
if (expandButton) {
|
|
expandButton.setAttribute('aria-pressed', 'true');
|
|
}
|
|
}
|
|
|
|
output.innerHTML = formattedTree;
|
|
syncButtons();
|
|
setAllSectionsExpanded(true);
|
|
|
|
if (collapseButton) {
|
|
collapseButton.addEventListener('click', function () {
|
|
setAllSectionsExpanded(false);
|
|
syncButtons();
|
|
});
|
|
}
|
|
|
|
if (expandButton) {
|
|
expandButton.addEventListener('click', function () {
|
|
setAllSectionsExpanded(true);
|
|
syncButtons();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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, element.getAttribute('data-local-datetime-format') === '24h');
|
|
});
|
|
}
|
|
|
|
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();
|
|
initDeleteButtons();
|
|
initDirtyTracking();
|
|
initLimitedInputValues();
|
|
initLimitedTextInputValues();
|
|
initCancelConfirm();
|
|
initAsyncCommandForms();
|
|
initAsyncSaveForms();
|
|
initSubmitOnChange();
|
|
initJsonTogglePanels();
|
|
initLocalDateTimes();
|
|
initTableSearches();
|
|
attachSlideThumbFallbacks(document);
|
|
window.initJsonTogglePanels = initJsonTogglePanels;
|
|
window.initLocalDateTimes = initLocalDateTimes;
|
|
window.clampLimitedInputValue = clampLimitedInputValue;
|
|
window.initLimitedInputValues = initLimitedInputValues;
|
|
window.clampLimitedTextValue = clampLimitedTextValue;
|
|
window.initLimitedTextInputValues = initLimitedTextInputValues;
|
|
window.createSlideThumbPlaceholder = createSlideThumbPlaceholder;
|
|
window.attachSlideThumbFallbacks = attachSlideThumbFallbacks;
|
|
}());
|