Implement video region support
This commit is contained in:
@@ -60,6 +60,9 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
var submitting = false;
|
||||
var uploadMaxBytes = 100 * 1024 * 1024;
|
||||
var uploadMaxLabel = '100 MB';
|
||||
var uploadVideoMaxBytes = 1024 * 1024 * 1024;
|
||||
var uploadVideoMaxLabel = '1 GB';
|
||||
var videoDurationCache = Object.create(null);
|
||||
var previewRenderFrame = 0;
|
||||
var previewPopupWindow = null;
|
||||
var previewPopupRenderFrame = 0;
|
||||
@@ -255,6 +258,12 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return current && current.value !== undefined ? current.value : '';
|
||||
}
|
||||
|
||||
function getCurrentRegionVideoDuration(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var duration = Math.round(Number(current.duration_seconds || 0) * 1000) / 1000;
|
||||
return Number.isFinite(duration) && duration > 0 ? String(duration) : '';
|
||||
}
|
||||
|
||||
function normalizeEditorData(value) {
|
||||
return String(value || '');
|
||||
}
|
||||
@@ -458,6 +467,47 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return '<iframe class="slide-preview-html-frame" sandbox="" scrolling="no" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="HTML preview" loading="eager" style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"></iframe>';
|
||||
}
|
||||
|
||||
function startPreviewVideoPlayback(root) {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
var videos = root.querySelectorAll('video.slide-preview-video');
|
||||
Array.prototype.forEach.call(videos, function (video) {
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
video.autoplay = true;
|
||||
video.loop = true;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
|
||||
function tryPlay() {
|
||||
try {
|
||||
video.load();
|
||||
} catch (_error) {
|
||||
// Ignore load failures and try to play anyway.
|
||||
}
|
||||
|
||||
var playPromise = video.play && video.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(function () {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (video.readyState >= 2) {
|
||||
tryPlay();
|
||||
return;
|
||||
}
|
||||
|
||||
video.addEventListener('canplay', tryPlay, { once: true });
|
||||
video.addEventListener('loadedmetadata', tryPlay, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function destroyEditors() {
|
||||
editorInstances.forEach(function (editor) {
|
||||
if (editor && typeof editor.destroy === 'function') {
|
||||
@@ -767,7 +817,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
function clearImagePreviewUrls() {
|
||||
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
|
||||
templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]').forEach(function (input) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
@@ -775,14 +825,41 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
});
|
||||
}
|
||||
|
||||
function getRegionMediaType(card) {
|
||||
if (card && card.dataset && String(card.dataset.regionMediaType || '').trim()) {
|
||||
return String(card.dataset.regionMediaType || '').trim() === 'video' ? 'video' : 'image';
|
||||
}
|
||||
|
||||
var regionTypeInput = card && card.querySelector ? card.querySelector('input[type="hidden"][name="region_type[]"]') : null;
|
||||
return regionTypeInput && String(regionTypeInput.value || '').trim() === 'video' ? 'video' : 'image';
|
||||
}
|
||||
|
||||
function getRegionMediaPrefix(mediaType) {
|
||||
return String(mediaType || '').trim() === 'video' ? 'region_video_' : 'region_image_';
|
||||
}
|
||||
|
||||
function getRegionExistingMediaPrefix(mediaType) {
|
||||
return String(mediaType || '').trim() === 'video' ? 'existing_region_video_' : 'existing_region_image_';
|
||||
}
|
||||
|
||||
function getRegionExistingVideoDurationName(regionId) {
|
||||
return 'existing_region_video_duration_' + regionId;
|
||||
}
|
||||
|
||||
function getRegionVideoDurationHiddenInput(regionId) {
|
||||
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
||||
return card ? card.querySelector('input[type="hidden"][name="' + getRegionExistingVideoDurationName(regionId) + '"]') : null;
|
||||
}
|
||||
|
||||
function getRegionUploadZone(input) {
|
||||
return input ? input.closest('[data-region-upload-zone]') : null;
|
||||
}
|
||||
|
||||
function getRegionUploadHiddenValue(input) {
|
||||
var regionId = input && input.name ? input.name.replace(/^region_image_/, '') : '';
|
||||
var mediaType = input && input.name && input.name.indexOf('region_video_') === 0 ? 'video' : 'image';
|
||||
var regionId = input && input.name ? input.name.replace(/^region_(?:image|video)_/, '') : '';
|
||||
var card = regionId ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="' + getRegionExistingMediaPrefix(mediaType) + regionId + '"]');
|
||||
return hidden ? hidden.value : '';
|
||||
}
|
||||
|
||||
@@ -847,21 +924,42 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return Boolean(file) && Number(file.size || 0) <= uploadMaxBytes;
|
||||
}
|
||||
|
||||
function getRegionMediaTypeFromInput(input) {
|
||||
return input && input.name && input.name.indexOf('region_video_') === 0 ? 'video' : 'image';
|
||||
}
|
||||
|
||||
function getRegionMediaUploadLimitBytes(mediaType) {
|
||||
return String(mediaType || '').trim() === 'video' ? uploadVideoMaxBytes : uploadMaxBytes;
|
||||
}
|
||||
|
||||
function getRegionMediaUploadLimitLabel(mediaType) {
|
||||
return String(mediaType || '').trim() === 'video' ? uploadVideoMaxLabel : uploadMaxLabel;
|
||||
}
|
||||
|
||||
function isRegionMediaFileWithinLimit(file, mediaType) {
|
||||
return Boolean(file) && Number(file.size || 0) <= getRegionMediaUploadLimitBytes(mediaType);
|
||||
}
|
||||
|
||||
function rejectOversizeRegionImage(input, file) {
|
||||
return rejectOversizeRegionMedia(input, file, 'image');
|
||||
}
|
||||
|
||||
function rejectOversizeRegionMedia(input, file, mediaType) {
|
||||
var zone = getRegionUploadZone(input);
|
||||
var hiddenValue = getRegionUploadHiddenValue(input);
|
||||
var limitLabel = getRegionMediaUploadLimitLabel(mediaType || getRegionMediaTypeFromInput(input));
|
||||
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast('File must be ' + uploadMaxLabel + ' or smaller.', 'warning');
|
||||
window.showToast('File must be ' + limitLabel + ' or smaller.', 'warning');
|
||||
} else {
|
||||
window.alert('File must be ' + uploadMaxLabel + ' or smaller.');
|
||||
window.alert('File must be ' + limitLabel + ' or smaller.');
|
||||
}
|
||||
input.value = '';
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
renderImageRegionPreview(zone ? zone.closest('[data-region-id]') : null, hiddenValue);
|
||||
renderRegionMediaPreview(zone ? zone.closest('[data-region-id]') : null, hiddenValue);
|
||||
if (file) {
|
||||
return false;
|
||||
}
|
||||
@@ -870,12 +968,18 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
function setRegionImageInputFile(input, file) {
|
||||
return setRegionMediaInputFile(input, file);
|
||||
}
|
||||
|
||||
function setRegionMediaInputFile(input, file) {
|
||||
if (!input || !file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRegionImageFileWithinLimit(file)) {
|
||||
rejectOversizeRegionImage(input, file);
|
||||
var mediaType = getRegionMediaTypeFromInput(input);
|
||||
|
||||
if (!isRegionMediaFileWithinLimit(file, mediaType)) {
|
||||
rejectOversizeRegionMedia(input, file, mediaType);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -886,18 +990,24 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
function uploadRegionImageFile(input, file) {
|
||||
return uploadRegionMediaFile(input, file);
|
||||
}
|
||||
|
||||
function uploadRegionMediaFile(input, file) {
|
||||
if (!input || !file) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (!isRegionImageFileWithinLimit(file)) {
|
||||
rejectOversizeRegionImage(input, file);
|
||||
var mediaType = getRegionMediaTypeFromInput(input);
|
||||
|
||||
if (!isRegionMediaFileWithinLimit(file, mediaType)) {
|
||||
rejectOversizeRegionMedia(input, file, mediaType);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
var regionId = input.name.replace(/^region_image_/, '');
|
||||
var regionId = input.name.replace(/^region_(?:image|video)_/, '');
|
||||
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="' + getRegionExistingMediaPrefix(mediaType) + regionId + '"]');
|
||||
var zone = getRegionUploadZone(input);
|
||||
var uploadToken = String(Date.now()) + ':' + Math.random().toString(16).slice(2);
|
||||
var previousUploadedPath = String(input.dataset.uploadedPath || '').trim();
|
||||
@@ -945,7 +1055,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
var error = new Error(response.responseText || 'Unable to upload image.');
|
||||
var error = new Error(response.responseText || 'Unable to upload media.');
|
||||
error.status = response.status;
|
||||
error.responseText = response.responseText;
|
||||
reject(error);
|
||||
@@ -961,7 +1071,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
|
||||
var uploadedPath = String(payload.path || '').trim();
|
||||
if (!uploadedPath) {
|
||||
reject(new Error('Unable to upload image.'));
|
||||
reject(new Error('Unable to upload media.'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -977,7 +1087,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
input.dataset.uploadedNeedsCleanup = '1';
|
||||
input.value = '';
|
||||
input.setCustomValidity('');
|
||||
renderImageRegionPreview(card, uploadedPath);
|
||||
renderRegionMediaPreview(card, uploadedPath);
|
||||
void syncVideoRegionDuration(card, uploadedPath);
|
||||
templateSelectorLock.markEdited();
|
||||
requestPreviewRender();
|
||||
if (previousNeedsCleanup && previousUploadedPath && previousUploadedPath !== uploadedPath) {
|
||||
@@ -993,7 +1104,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Unable to upload image.'));
|
||||
reject(new Error('Unable to upload media.'));
|
||||
};
|
||||
|
||||
xhr.onabort = function () {
|
||||
@@ -1002,7 +1113,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Unable to upload image.'));
|
||||
reject(new Error('Unable to upload media.'));
|
||||
};
|
||||
|
||||
xhr.onloadend = function () {
|
||||
@@ -1023,7 +1134,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
function getPendingUploadCleanupPaths() {
|
||||
return getRegionImageInputs().map(function (input) {
|
||||
return getRegionMediaInputs().map(function (input) {
|
||||
if (!input || String(input.dataset.uploadedNeedsCleanup || '') !== '1') {
|
||||
return null;
|
||||
}
|
||||
@@ -1033,7 +1144,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
function clearPendingUploadCleanupPaths() {
|
||||
getRegionImageInputs().forEach(function (input) {
|
||||
getRegionMediaInputs().forEach(function (input) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
@@ -1077,7 +1188,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
});
|
||||
}
|
||||
|
||||
function renderImageRegionPreview(card, value) {
|
||||
function renderRegionMediaPreview(card, value) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
@@ -1087,13 +1198,148 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaType = getRegionMediaType(card);
|
||||
var isVideo = mediaType === 'video';
|
||||
|
||||
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>';
|
||||
? '<div class="slide-image-region-preview-shell" data-remove-' + (isVideo ? 'region-video' : 'region-image') + '="' + escapeHtml(card.getAttribute('data-region-id') || '') + '" role="button" tabindex="0" aria-label="Remove ' + (isVideo ? 'video' : 'image') + '">' +
|
||||
(isVideo
|
||||
? '<video class="slide-image-region-preview" src="' + escapeHtml(value) + '" autoplay loop muted playsinline preload="metadata"></video>'
|
||||
: '<img class="slide-image-region-preview" src="' + escapeHtml(value) + '" 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 ' + (isVideo ? 'video' : 'image') + '</div>';
|
||||
|
||||
if (isVideo) {
|
||||
window.requestAnimationFrame(function () {
|
||||
var video = previewBox.querySelector('video.slide-image-region-preview');
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
video.load();
|
||||
} catch (_error) {
|
||||
// Ignore load failures; play() will retry if the browser allows it.
|
||||
}
|
||||
var playPromise = video.play && video.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(function () {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadVideoDuration(mediaPath) {
|
||||
var cacheKey = String(mediaPath || '').trim();
|
||||
var cachedPromise;
|
||||
|
||||
if (!cacheKey) {
|
||||
return Promise.reject(new Error('No video source is available for this slide.'));
|
||||
}
|
||||
|
||||
cachedPromise = videoDurationCache[cacheKey];
|
||||
if (cachedPromise) {
|
||||
return cachedPromise;
|
||||
}
|
||||
|
||||
cachedPromise = new Promise(function (resolve, reject) {
|
||||
var video = document.createElement('video');
|
||||
var timeoutId = window.setTimeout(function () {
|
||||
cleanup();
|
||||
reject(new Error('Timed out loading the video metadata.'));
|
||||
}, 15000);
|
||||
|
||||
function cleanup() {
|
||||
window.clearTimeout(timeoutId);
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
if (video.parentNode) {
|
||||
video.parentNode.removeChild(video);
|
||||
}
|
||||
}
|
||||
|
||||
video.preload = 'metadata';
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.setAttribute('playsinline', '');
|
||||
video.setAttribute('muted', '');
|
||||
video.style.position = 'absolute';
|
||||
video.style.left = '-9999px';
|
||||
video.style.top = '0';
|
||||
video.style.width = '1px';
|
||||
video.style.height = '1px';
|
||||
video.style.opacity = '0';
|
||||
video.style.pointerEvents = 'none';
|
||||
|
||||
video.addEventListener('loadedmetadata', function () {
|
||||
var duration = Number(video.duration);
|
||||
cleanup();
|
||||
if (Number.isFinite(duration) && duration > 0) {
|
||||
resolve(Math.max(1, Math.round(duration)));
|
||||
} else {
|
||||
reject(new Error('The video duration could not be read.'));
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
video.addEventListener('error', function () {
|
||||
cleanup();
|
||||
reject(new Error('The video duration could not be read.'));
|
||||
}, { once: true });
|
||||
|
||||
video.addEventListener('abort', function () {
|
||||
cleanup();
|
||||
reject(new Error('The video duration could not be read.'));
|
||||
}, { once: true });
|
||||
|
||||
document.body.appendChild(video);
|
||||
video.src = cacheKey;
|
||||
video.load();
|
||||
}).catch(function (error) {
|
||||
delete videoDurationCache[cacheKey];
|
||||
throw error;
|
||||
});
|
||||
|
||||
videoDurationCache[cacheKey] = cachedPromise;
|
||||
return cachedPromise;
|
||||
}
|
||||
|
||||
async function syncVideoRegionDuration(card, source) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var regionId = card.getAttribute('data-region-id');
|
||||
var hidden = regionId ? getRegionVideoDurationHiddenInput(regionId) : null;
|
||||
|
||||
if (!hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaPath = String(source || '').trim();
|
||||
if (!mediaPath) {
|
||||
hidden.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
hidden.value = String(await loadVideoDuration(mediaPath));
|
||||
} catch (_error) {
|
||||
hidden.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderImageRegionPreview(card, value) {
|
||||
return renderRegionMediaPreview(card, value);
|
||||
}
|
||||
|
||||
function getRegionImageInputs() {
|
||||
return Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"]'));
|
||||
return Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]'));
|
||||
}
|
||||
|
||||
function getRegionMediaInputs() {
|
||||
return getRegionImageInputs();
|
||||
}
|
||||
|
||||
function hasPendingRegionImageUpload() {
|
||||
@@ -1102,6 +1348,10 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
});
|
||||
}
|
||||
|
||||
function hasPendingRegionMediaUpload() {
|
||||
return hasPendingRegionImageUpload();
|
||||
}
|
||||
|
||||
function getRegionBox(region, canvasWidth, canvasHeight) {
|
||||
return {
|
||||
left: Math.max(0, Math.round(Number(region.x || 0))),
|
||||
@@ -1177,6 +1427,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
if (popupOverlay) {
|
||||
popupOverlay.innerHTML = '';
|
||||
}
|
||||
startPreviewVideoPlayback(popupCanvas);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1198,6 +1449,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
popupEmpty.style.display = 'none';
|
||||
}
|
||||
|
||||
startPreviewVideoPlayback(popupCanvas);
|
||||
|
||||
var stageRect = popupStage.getBoundingClientRect();
|
||||
if (!stageRect.width || !stageRect.height) {
|
||||
if (previewPopupRenderFrame) {
|
||||
@@ -1349,6 +1602,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
var htmlInput = card && card.querySelector('textarea[name="region_html_' + region.id + '"]');
|
||||
var imageHidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
|
||||
var imageFileInput = card && card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
|
||||
var videoHidden = card && card.querySelector('input[type="hidden"][name="existing_region_video_' + region.id + '"]');
|
||||
var videoFileInput = card && card.querySelector('input[type="file"][name="region_video_' + region.id + '"]');
|
||||
var webpageInput = card && card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
|
||||
var rtmpInput = card && card.querySelector('input[type="url"][name="region_rtmp_' + region.id + '"]');
|
||||
var disableAudioInput = card && card.querySelector('input[type="checkbox"][name="region_disable_audio_' + region.id + '"]');
|
||||
@@ -1361,6 +1616,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
var existingApi = existingContent[region.region_key] || {};
|
||||
var value = region.region_type === 'image'
|
||||
? ((imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : ''))
|
||||
: region.region_type === 'video'
|
||||
? ((videoFileInput && videoFileInput.dataset.previewUrl) ? videoFileInput.dataset.previewUrl : (videoHidden ? videoHidden.value : ''))
|
||||
: region.region_type === 'webpage'
|
||||
? (webpageInput ? webpageInput.value : '')
|
||||
: region.region_type === 'rtmp'
|
||||
@@ -1383,6 +1640,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
: getTextStyleFromCard(card, region);
|
||||
var content = region.region_type === 'image'
|
||||
? (value ? '<img class="slide-preview-image" src="' + escapeHtml(value) + '" alt="" />' : '<div class="slide-preview-placeholder">Image</div>')
|
||||
: region.region_type === 'video'
|
||||
? (value ? '<video class="slide-preview-video" src="' + escapeHtml(value) + '" autoplay loop muted playsinline preload="metadata"></video>' : '<div class="slide-preview-placeholder">Video</div>')
|
||||
: region.region_type === 'webpage'
|
||||
? renderPreviewWebpageRegion(value)
|
||||
: region.region_type === 'rtmp'
|
||||
@@ -1407,6 +1666,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
: renderPreviewTextRegion(region, value, style, scale);
|
||||
var selected = region.region_type === 'image'
|
||||
? ' slide-preview-image-region'
|
||||
: region.region_type === 'video'
|
||||
? ' slide-preview-video-region'
|
||||
: region.region_type === 'webpage'
|
||||
? ' slide-preview-webpage-region'
|
||||
: region.region_type === 'rtmp'
|
||||
@@ -1421,6 +1682,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return '<div class="slide-preview-region' + selected + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px;">' + content + '</div>';
|
||||
}).join('');
|
||||
|
||||
startPreviewVideoPlayback(slidePreviewOverlay);
|
||||
|
||||
syncPopupPreview();
|
||||
}
|
||||
|
||||
@@ -1610,7 +1873,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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 card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" data-region-media-type="image">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Image</span>' +
|
||||
@@ -1652,6 +1915,53 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderVideoRegion(region) {
|
||||
var current = getCurrentRegionValue(region);
|
||||
var currentDuration = getCurrentRegionVideoDuration(region);
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" data-region-media-type="video">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Video</span>' +
|
||||
'</div>' +
|
||||
'<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_video_' + region.id + '">' +
|
||||
'<input type="file" id="region_video_' + region.id + '" name="region_video_' + region.id + '" class="visually-hidden" accept="video/*" />' +
|
||||
'<span class="slide-image-region-upload-zone-content">' +
|
||||
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-camera-video" aria-hidden="true"></i></span>' +
|
||||
'<span class="slide-image-region-upload-zone-copy">' +
|
||||
'<strong>Drop a video here or click to upload</strong>' +
|
||||
'<span>MP4, WebM, or Ogg</span>' +
|
||||
'<span class="slide-image-region-upload-zone-limit">Max ' + uploadVideoMaxLabel + ' per file</span>' +
|
||||
'</span>' +
|
||||
'</span>' +
|
||||
'<span class="slide-image-region-upload-zone-progress" data-region-upload-progress hidden>' +
|
||||
'<span class="slide-image-region-upload-zone-progress-label" data-region-upload-progress-text>Uploading...</span>' +
|
||||
'<div class="progress slide-image-region-upload-zone-progress-bar" role="progressbar" aria-label="Video upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
|
||||
'<div class="progress-bar bg-danger progress-bar-striped progress-bar-animated" data-region-upload-progress-bar style="width:0%">0%</div>' +
|
||||
'</div>' +
|
||||
'</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-video="' + region.id + '" role="button" tabindex="0" aria-label="Remove video">' +
|
||||
'<video class="slide-image-region-preview" src="' + escapeHtml(current) + '" muted playsinline preload="metadata"></video>' +
|
||||
'<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 video</div>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="existing_region_video_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
||||
'<input type="hidden" name="' + getRegionExistingVideoDurationName(region.id) + '" value="' + escapeHtml(currentDuration) + '" />' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderTemplate() {
|
||||
templateSelectorLock.reset();
|
||||
clearImagePreviewUrls();
|
||||
@@ -1667,6 +1977,9 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
if (region.region_type === 'image') {
|
||||
return renderImageRegion(region);
|
||||
}
|
||||
if (region.region_type === 'video') {
|
||||
return renderVideoRegion(region);
|
||||
}
|
||||
if (region.region_type === 'webpage') {
|
||||
return renderWebpageRegion(region);
|
||||
}
|
||||
@@ -1685,6 +1998,14 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
return renderTextRegion(region);
|
||||
}).join('') : '<div class="muted">This template has no regions.</div>';
|
||||
|
||||
templateFields.querySelectorAll('[data-region-media-type="video"]').forEach(function (card) {
|
||||
var regionId = card.getAttribute('data-region-id');
|
||||
var hidden = regionId ? card.querySelector('input[type="hidden"][name="existing_region_video_' + regionId + '"]') : null;
|
||||
if (hidden && hidden.value) {
|
||||
void syncVideoRegionDuration(card, hidden.value);
|
||||
}
|
||||
});
|
||||
|
||||
templateFields.querySelectorAll('.ckeditor-holder').forEach(function (holder) {
|
||||
var regionId = holder.getAttribute('data-region-id');
|
||||
var source = holder.querySelector('.ckeditor-source');
|
||||
@@ -1792,25 +2113,26 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
});
|
||||
});
|
||||
|
||||
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
|
||||
templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]').forEach(function (input) {
|
||||
input.addEventListener('change', function () {
|
||||
var regionId = input.name.replace(/^region_image_/, '');
|
||||
var mediaType = getRegionMediaTypeFromInput(input);
|
||||
var regionId = input.name.replace(/^region_(?:image|video)_/, '');
|
||||
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + regionId + '"]');
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="' + getRegionExistingMediaPrefix(mediaType) + regionId + '"]');
|
||||
var file = input.files && input.files.length ? input.files[0] : null;
|
||||
|
||||
if (file && !isRegionImageFileWithinLimit(file)) {
|
||||
if (file && !isRegionMediaFileWithinLimit(file, mediaType)) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
input.value = '';
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast('File must be ' + uploadMaxLabel + ' or smaller.', 'warning');
|
||||
window.showToast('File must be ' + getRegionMediaUploadLimitLabel(mediaType) + ' or smaller.', 'warning');
|
||||
} else {
|
||||
window.alert('File must be ' + uploadMaxLabel + ' or smaller.');
|
||||
window.alert('File must be ' + getRegionMediaUploadLimitLabel(mediaType) + ' or smaller.');
|
||||
}
|
||||
renderImageRegionPreview(card, hidden ? hidden.value : '');
|
||||
renderRegionMediaPreview(card, hidden ? hidden.value : '');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1821,22 +2143,24 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
if (file) {
|
||||
input.dataset.previewUrl = URL.createObjectURL(file);
|
||||
}
|
||||
renderImageRegionPreview(card, input.dataset.previewUrl || (hidden ? hidden.value : ''));
|
||||
renderRegionMediaPreview(card, input.dataset.previewUrl || (hidden ? hidden.value : ''));
|
||||
void syncVideoRegionDuration(card, input.dataset.previewUrl || (hidden ? hidden.value : ''));
|
||||
templateSelectorLock.markEdited();
|
||||
requestPreviewRender();
|
||||
if (file) {
|
||||
uploadRegionImageFile(input, file).catch(function (error) {
|
||||
uploadRegionMediaFile(input, file).catch(function (error) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
input.value = '';
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(error && error.message ? error.message : 'Unable to upload image.', 'danger');
|
||||
showToast(error && error.message ? error.message : 'Unable to upload media.', 'danger');
|
||||
} else {
|
||||
window.alert(error && error.message ? error.message : 'Unable to upload image.');
|
||||
window.alert(error && error.message ? error.message : 'Unable to upload media.');
|
||||
}
|
||||
renderImageRegionPreview(card, hidden ? hidden.value : '');
|
||||
renderRegionMediaPreview(card, hidden ? hidden.value : '');
|
||||
void syncVideoRegionDuration(card, hidden ? hidden.value : '');
|
||||
requestPreviewRender();
|
||||
});
|
||||
}
|
||||
@@ -1844,7 +2168,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
});
|
||||
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 + '"]');
|
||||
var input = zone.querySelector('input[type="file"]');
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
@@ -1864,24 +2188,27 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
|
||||
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 || '')) {
|
||||
var card = zone.closest('[data-region-id]');
|
||||
var mediaType = getRegionMediaType(card);
|
||||
if (!file || !(mediaType === 'video' ? /^video\//i.test(file.type || '') : /^image\//i.test(file.type || ''))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRegionImageFileWithinLimit(file)) {
|
||||
rejectOversizeRegionImage(input, file);
|
||||
if (!isRegionMediaFileWithinLimit(file, mediaType)) {
|
||||
rejectOversizeRegionMedia(input, file, mediaType);
|
||||
return;
|
||||
}
|
||||
|
||||
setRegionImageInputFile(input, file);
|
||||
setRegionMediaInputFile(input, file);
|
||||
});
|
||||
});
|
||||
templateFields.querySelectorAll('button[data-remove-region-image]').forEach(function (button) {
|
||||
templateFields.querySelectorAll('[data-remove-region-image], [data-remove-region-video]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var regionId = button.getAttribute('data-remove-region-image');
|
||||
var regionId = button.getAttribute('data-remove-region-image') || button.getAttribute('data-remove-region-video');
|
||||
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 + '"]');
|
||||
var mediaType = getRegionMediaType(card);
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="' + getRegionExistingMediaPrefix(mediaType) + regionId + '"]');
|
||||
var input = card && card.querySelector('input[type="file"][name="' + getRegionMediaPrefix(mediaType) + regionId + '"]');
|
||||
var uploadPath = input ? String(input.dataset.uploadedPath || '').trim() : '';
|
||||
|
||||
if (input && input.dataset.previewUrl) {
|
||||
@@ -1899,48 +2226,16 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
if (hidden) {
|
||||
hidden.value = '';
|
||||
}
|
||||
var durationHidden = card ? getRegionVideoDurationHiddenInput(regionId) : null;
|
||||
if (durationHidden) {
|
||||
durationHidden.value = '';
|
||||
}
|
||||
|
||||
renderImageRegionPreview(card, '');
|
||||
renderRegionMediaPreview(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 + '"]');
|
||||
var uploadPath = input ? String(input.dataset.uploadedPath || '').trim() : '';
|
||||
|
||||
if (input && input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
if (input && input.dataset.uploadedNeedsCleanup === '1' && uploadPath) {
|
||||
queueUploadCleanup([uploadPath]);
|
||||
}
|
||||
if (input) {
|
||||
input.value = '';
|
||||
delete input.dataset.uploadedPath;
|
||||
delete input.dataset.uploadedNeedsCleanup;
|
||||
}
|
||||
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)_/, '');
|
||||
|
||||
Reference in New Issue
Block a user