Release v2.4.2

This commit is contained in:
2026-08-03 12:45:27 +01:00
parent 2b9cabdab2
commit e95928f31c
141 changed files with 7694 additions and 5078 deletions
+315 -115
View File
@@ -19,7 +19,123 @@
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;
});
});
}
@@ -79,6 +195,191 @@
}, 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 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) {
throw new Error(await response.text() || (settings.errorMessage || 'Unable to save changes.'));
}
if (isLoginRedirect(response)) {
window.location.replace(response.url || '/login');
return true;
}
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 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
});
}
clearFormDirty(form);
var successMessage = typeof settings.getSuccessMessage === 'function'
? settings.getSuccessMessage({
form: form,
response: response,
responseText: responseText,
responseDocument: responseDocument,
submitterValue: submitterValue
})
: parseAsyncSaveResponseMessage(responseText);
showToast(successMessage || String(settings.fallbackSuccessMessage || 'Saved.'), 'success');
return true;
} catch (error) {
if (typeof showToast === 'function') {
var variant = Number(error && error.status) >= 400 && Number(error && error.status) < 500 ? '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);
@@ -174,7 +475,7 @@
return;
}
var hiddenInput = form.querySelector('input[type="hidden"][name="save_action"]');
var hiddenInput = getAsyncSaveActionField(form);
if (!hiddenInput) {
hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
@@ -208,6 +509,9 @@
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) {
@@ -311,7 +615,7 @@
return;
}
var button = target.closest('button[name="save_action"]');
var button = target.closest('button[name="save_action"], button[name="action"]');
if (!button) {
return;
}
@@ -330,129 +634,24 @@
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 = '';
}
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 && responseDocument) {
} else if (refreshTargetSelector && context.responseDocument) {
var currentTarget = document.querySelector(refreshTargetSelector);
var nextTarget = responseDocument.querySelector(refreshTargetSelector);
var nextTarget = context.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);
@@ -651,6 +850,7 @@
}
initConfirmForms();
initDeleteButtons();
initDirtyTracking();
initCancelConfirm();
initAsyncCommandForms();