Release 1.5.4

This commit is contained in:
2026-07-25 17:22:21 +01:00
parent 0e89892c94
commit c18f597068
9 changed files with 831 additions and 110 deletions
+156 -13
View File
@@ -773,6 +773,32 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
});
}
function setRegionImageInputFile(input, file) {
if (!input || !file) {
return;
}
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
input.files = dataTransfer.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
function renderImageRegionPreview(card, value) {
if (!card) {
return;
}
var previewBox = card.querySelector('.slide-image-region-preview-box');
if (!previewBox) {
return;
}
previewBox.innerHTML = value
? '<img class="slide-image-region-preview" src="' + escapeHtml(value) + '" alt="Current image preview" />'
: '<div class="slide-image-region-preview slide-image-region-preview-empty">No image</div>';
}
function getRegionBox(region, canvasWidth, canvasHeight) {
return {
left: Math.max(0, Math.round(Number(region.x || 0))),
@@ -1118,7 +1144,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'<span class="chip">Text</span>' +
'</div>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="card-body p-3 d-grid">' +
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current) + '</textarea>' +
'</div>' +
@@ -1148,7 +1174,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'<span class="chip">RSS</span>' +
'</div>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="card-body p-3 d-grid">' +
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
'</div>' +
@@ -1193,7 +1219,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'<span class="chip">API</span>' +
'</div>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="card-body p-3 d-grid">' +
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
'</div>' +
@@ -1226,7 +1252,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">Webpage</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="card-body p-3 d-grid">' +
'<label style="display:block;">Webpage URL' +
'<input type="url" name="region_webpage_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
'</label>' +
@@ -1245,7 +1271,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">RTMP</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="card-body p-3 d-grid">' +
'<label style="display:block;">RTMP URL' +
'<input type="url" name="region_rtmp_' + region.id + '" class="form-control" value="' + escapeHtml(url) + '" placeholder="rtmp://example.com/live/stream" />' +
'</label>' +
@@ -1266,7 +1292,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">HTML</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="card-body p-3 d-grid">' +
'<label style="display:block;">HTML content' +
'<textarea name="region_html_' + region.id + '" class="form-control" rows="10" placeholder="<div>Hello</div>">' + escapeHtml(current) + '</textarea>' +
'</label>' +
@@ -1275,22 +1301,53 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
'</div>';
}
function reduceAspectRatio(width, height) {
var a = Math.max(1, Math.round(Number(width || 0) || 1));
var b = Math.max(1, Math.round(Number(height || 0) || 1));
while (b !== 0) {
var remainder = a % b;
a = b;
b = remainder;
}
var divisor = Math.max(1, a);
return Math.max(1, Math.round(Number(width || 0) || 1) / divisor) + ':' + Math.max(1, Math.round(Number(height || 0) || 1) / divisor);
}
function renderImageRegion(region) {
var current = getCurrentRegionValue(region);
var regionWidth = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
var regionHeight = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
var regionRatio = reduceAspectRatio(regionWidth, regionHeight);
return '' +
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
'<div class="card-header template-field-head">' +
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">Image</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<label style="display:block;">Image file' +
'<input type="file" name="region_image_' + region.id + '" class="form-control" accept="image/*" />' +
'</label>' +
'<div class="card-body p-3 d-grid">' +
'<div class="row g-3 align-items-start">' +
'<div class="col-12 col-md-8 d-flex flex-column">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" for="region_image_' + region.id + '">' +
'<input type="file" id="region_image_' + region.id + '" name="region_image_' + region.id + '" class="visually-hidden" accept="image/*" data-slide-image-cropper-region-ratio="' + escapeHtml(regionRatio) + '" data-slide-image-cropper-region-ratio-label="Region" />' +
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>' +
'<span class="slide-image-region-upload-zone-copy">' +
'<strong>Drop an image here or click to upload</strong>' +
'<span>PNG, JPG, GIF, or WebP</span>' +
'</span>' +
'</label>' +
'</div>' +
'<div class="col-12 col-md-4">' +
'<div class="slide-image-region-preview-box">' +
(current
? '<div class="slide-image-region-preview-shell" data-remove-region-image="' + region.id + '" role="button" tabindex="0" aria-label="Remove image">' +
'<img class="slide-image-region-preview" src="' + escapeHtml(current) + '" alt="Current image preview" />' +
'<span class="slide-image-region-preview-remove" aria-hidden="true"><i class="bi bi-trash3" aria-hidden="true"></i></span>' +
'</div>'
: '<div class="slide-image-region-preview slide-image-region-preview-empty">No image</div>') +
'</div>' +
'</div>' +
'</div>' +
'<input type="hidden" name="existing_region_image_' + region.id + '" value="' + escapeHtml(current) + '" />' +
(current
? '<div class="muted slide-image-file">Current: <a href="' + escapeHtml(current) + '" target="_blank" rel="noreferrer">' + escapeHtml(current) + '</a></div>'
: '<div class="muted slide-image-file">No image selected.</div>') +
'</div>' +
'</div>';
}
@@ -1444,10 +1501,96 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
if (input.files && input.files.length) {
input.dataset.previewUrl = URL.createObjectURL(input.files[0]);
}
var regionId = input.name.replace(/^region_image_/, '');
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
var hidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
renderImageRegionPreview(card, input.dataset.previewUrl || (hidden ? hidden.value : ''));
templateSelectorLock.markEdited();
requestPreviewRender();
});
});
templateFields.querySelectorAll('[data-region-upload-zone]').forEach(function (zone) {
var regionId = zone.getAttribute('data-region-upload-zone');
var input = zone.querySelector('input[type="file"][name="region_image_' + regionId + '"]');
if (!input) {
return;
}
zone.addEventListener('dragover', function (event) {
event.preventDefault();
zone.classList.add('is-dragover');
});
zone.addEventListener('dragleave', function () {
zone.classList.remove('is-dragover');
});
zone.addEventListener('drop', function (event) {
event.preventDefault();
zone.classList.remove('is-dragover');
var files = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files : null;
var file = files && files.length ? files[0] : null;
if (!file || !/^image\//i.test(file.type || '')) {
return;
}
setRegionImageInputFile(input, file);
});
});
templateFields.querySelectorAll('button[data-remove-region-image]').forEach(function (button) {
button.addEventListener('click', function () {
var regionId = button.getAttribute('data-remove-region-image');
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
var hidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
var input = card && card.querySelector('input[type="file"][name="region_image_' + regionId + '"]');
if (input && input.dataset.previewUrl) {
URL.revokeObjectURL(input.dataset.previewUrl);
delete input.dataset.previewUrl;
}
if (input) {
input.value = '';
}
if (hidden) {
hidden.value = '';
}
renderImageRegionPreview(card, '');
templateSelectorLock.markEdited();
requestPreviewRender();
});
});
templateFields.querySelectorAll('.slide-image-region-preview-shell').forEach(function (shell) {
shell.addEventListener('click', function () {
var regionId = shell.getAttribute('data-remove-region-image');
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
var hidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
var input = card && card.querySelector('input[type="file"][name="region_image_' + regionId + '"]');
if (input && input.dataset.previewUrl) {
URL.revokeObjectURL(input.dataset.previewUrl);
delete input.dataset.previewUrl;
}
if (input) {
input.value = '';
}
if (hidden) {
hidden.value = '';
}
renderImageRegionPreview(card, '');
templateSelectorLock.markEdited();
requestPreviewRender();
});
shell.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
shell.click();
}
});
});
templateFields.querySelectorAll('select[name^="region_rss_feed_id_"], input[name^="region_rss_item_number_"], select[name^="region_api_source_id_"], input[name^="region_api_item_number_"]').forEach(function (input) {
input.addEventListener('input', function () {
var regionId = input.name.replace(/^region_(?:rss_feed_id|rss_item_number|api_source_id|api_item_number)_/, '');