// Shared browser utilities for region helper modules. (function () { function escapeHtml(value) { if (window.webUiHelpers && typeof window.webUiHelpers.escapeHtml === 'function') { return window.webUiHelpers.escapeHtml(value); } return String(value === undefined || value === null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function sanitizePreviewHtml(html) { var output = String(html || ''); output = output.replace(//gi, ''); output = output.replace(//gi, ''); return output.replace(/<[^>]+>/g, function (tag) { var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i); if (!match) { return ''; } var closing = Boolean(match[1]); var name = String(match[2] || '').toLowerCase(); var attrText = String(match[3] || ''); var selfClosing = Boolean(match[4]) || name === 'br' || name === 'hr'; var allowed = ['a', 'b', 'blockquote', 'br', 'code', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul']; if (allowed.indexOf(name) === -1) { return ''; } if (closing) { return ''; } return '<' + name + sanitizeTagAttributes(name, attrText) + '>'; }); } function sanitizeRichText(html) { return sanitizePreviewHtml(html); } function sanitizeTagAttributes(tagName, attrText) { var allowedAttributes = { a: ['href', 'title', 'target', 'rel', 'class', 'style'], blockquote: ['class', 'style'], div: ['class', 'style'], figure: ['class', 'style'], figcaption: ['class', 'style'], h1: ['class', 'style'], h2: ['class', 'style'], h3: ['class', 'style'], h4: ['class', 'style'], h5: ['class', 'style'], h6: ['class', 'style'], li: ['class', 'style'], ol: ['class', 'style', 'start'], p: ['class', 'style'], pre: ['class', 'style'], span: ['class', 'style'], table: ['class', 'style'], td: ['class', 'style', 'colspan', 'rowspan'], th: ['class', 'style', 'colspan', 'rowspan', 'scope'], tr: ['class', 'style'], ul: ['class', 'style'] }; var allowed = allowedAttributes[tagName] || []; if (!allowed.length) { return ''; } var attrs = []; attrText.replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, function (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) { var lowerKey = String(key || '').toLowerCase(); if (allowed.indexOf(lowerKey) === -1) { return ''; } var value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : ''; if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) { return ''; } if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) { return ''; } if (lowerKey === 'target') { var targetValue = String(value || '').trim(); if (targetValue === '_blank') { attrs.push(' target="_blank"'); if (attrs.indexOf(' rel="noreferrer noopener"') === -1) { attrs.push(' rel="noreferrer noopener"'); } return ''; } } attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"'); return ''; }); return attrs.join(''); } function sanitizeFontFamily(value) { return String(value || '').trim(); } function sanitizeTextColor(value) { return String(value || '').trim() || '#000000'; } function normalizeAcceptList(value) { return Array.isArray(value) ? value : String(value || '').split(',').map(function (item) { return String(item || '').trim(); }).filter(Boolean); } function fileMatchesAccept(file, acceptValue) { if (!file) { return false; } var acceptList = normalizeAcceptList(acceptValue).map(function (item) { return item.toLowerCase(); }); if (!acceptList.length) { return true; } var fileName = String(file.name || '').toLowerCase(); var fileType = String(file.type || '').toLowerCase(); return acceptList.some(function (rule) { if (rule === '*/*') { return true; } if (rule.charAt(0) === '.') { return fileName.endsWith(rule); } if (rule.endsWith('/*')) { return fileType.indexOf(rule.slice(0, -1)) === 0; } return fileType === rule; }); } function bindRegionMediaRemoveControls(templateFields, options) { var callbacks = options || {}; if (!templateFields || templateFields.dataset.regionMediaRemoveBound === '1') { return; } templateFields.dataset.regionMediaRemoveBound = '1'; function getCard(regionId) { if (typeof callbacks.getCardByRegionId === 'function') { return callbacks.getCardByRegionId(regionId); } return regionId ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null; } function getMediaType(button, card) { if (typeof callbacks.getMediaType === 'function') { return callbacks.getMediaType(card, button); } if (button && button.hasAttribute('data-remove-region-video')) { return 'video'; } if (button && button.hasAttribute('data-remove-region-qr-image')) { return 'qr-image'; } if (button && button.hasAttribute('data-remove-region-image')) { return 'image'; } if (card && card.dataset && String(card.dataset.regionMediaType || '').trim()) { var regionMediaType = String(card.dataset.regionMediaType || '').trim(); if (regionMediaType === 'video') { return 'video'; } if (regionMediaType === 'qr-image') { return 'qr-image'; } return 'image'; } return 'image'; } function getExistingPrefix(mediaType) { if (typeof callbacks.getExistingMediaPrefix === 'function') { return callbacks.getExistingMediaPrefix(mediaType); } if (String(mediaType || '').trim() === 'video') { return 'existing_region_video_'; } if (String(mediaType || '').trim() === 'qr-image') { return 'existing_region_qr_image_'; } return 'existing_region_image_'; } function getMediaPrefix(mediaType) { if (typeof callbacks.getMediaPrefix === 'function') { return callbacks.getMediaPrefix(mediaType); } if (String(mediaType || '').trim() === 'video') { return 'region_video_'; } if (String(mediaType || '').trim() === 'qr-image') { return 'region_qr_image_'; } return 'region_image_'; } function getVideoDurationHiddenInput(regionId) { if (typeof callbacks.getVideoDurationHiddenInput === 'function') { return callbacks.getVideoDurationHiddenInput(regionId); } return null; } function handleRemove(button) { var regionId = button.getAttribute('data-remove-region-image') || button.getAttribute('data-remove-region-video') || button.getAttribute('data-remove-region-qr-image'); var card = getCard(regionId); var mediaType = getMediaType(button, card); var hiddenName = getExistingPrefix(mediaType) + regionId; var inputName = getMediaPrefix(mediaType) + regionId; var hidden = card && card.querySelector('input[type="hidden"][name="' + hiddenName + '"]'); var input = card && card.querySelector('input[type="file"][name="' + inputName + '"]'); if (!hidden && regionId) { hidden = templateFields.querySelector('input[type="hidden"][name="' + hiddenName + '"]'); } if (!input && regionId) { input = templateFields.querySelector('input[type="file"][name="' + inputName + '"]'); } var uploadPath = input ? String(input.dataset.uploadedPath || '').trim() : ''; var hasPendingUpload = input && input.dataset && String(input.dataset.uploadToken || '').trim(); if (input && input.dataset.previewUrl) { URL.revokeObjectURL(input.dataset.previewUrl); delete input.dataset.previewUrl; } if (input && hasPendingUpload) { input.dataset.uploadCancelRequested = '1'; } if (input && input.dataset.uploadedNeedsCleanup === '1' && uploadPath && typeof callbacks.queueUploadCleanup === 'function') { callbacks.queueUploadCleanup([uploadPath]); } if (input) { input.value = ''; delete input.dataset.uploadedPath; delete input.dataset.uploadedNeedsCleanup; } if (hidden) { hidden.value = ''; hidden.dispatchEvent(new Event('change', { bubbles: true })); } var durationHidden = getVideoDurationHiddenInput(regionId); if (durationHidden) { durationHidden.value = ''; } if (typeof callbacks.renderPreview === 'function') { callbacks.renderPreview(card, ''); } if (typeof callbacks.markEdited === 'function') { callbacks.markEdited(); } if (typeof callbacks.requestPreviewRender === 'function') { callbacks.requestPreviewRender(); } } templateFields.addEventListener('click', function (event) { var button = event.target && event.target.closest ? event.target.closest('[data-remove-region-image], [data-remove-region-video], [data-remove-region-qr-image]') : null; if (!button || !templateFields.contains(button)) { return; } handleRemove(button); }); templateFields.addEventListener('keydown', function (event) { if (event.key !== 'Enter' && event.key !== ' ') { return; } var button = event.target && event.target.closest ? event.target.closest('[data-remove-region-image], [data-remove-region-video], [data-remove-region-qr-image]') : null; if (!button || !templateFields.contains(button)) { return; } event.preventDefault(); handleRemove(button); }); } function createRegionMediaUploadController(templateFields, options) { var callbacks = options || {}; if (!templateFields) { return null; } function getCard(regionId) { if (typeof callbacks.getCardByRegionId === 'function') { return callbacks.getCardByRegionId(regionId); } return regionId ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null; } function getMediaTypeFromInput(input) { if (typeof callbacks.getMediaTypeFromInput === 'function') { return callbacks.getMediaTypeFromInput(input); } if (input && input.name && input.name.indexOf('region_video_') === 0) { return 'video'; } if (input && input.name && input.name.indexOf('region_qr_image_') === 0) { return 'qr-image'; } return 'image'; } function getMediaPrefix(mediaType) { if (typeof callbacks.getMediaPrefix === 'function') { return callbacks.getMediaPrefix(mediaType); } if (String(mediaType || '').trim() === 'video') { return 'region_video_'; } if (String(mediaType || '').trim() === 'qr-image') { return 'region_qr_image_'; } return 'region_image_'; } function getExistingMediaPrefix(mediaType) { if (typeof callbacks.getExistingMediaPrefix === 'function') { return callbacks.getExistingMediaPrefix(mediaType); } if (String(mediaType || '').trim() === 'video') { return 'existing_region_video_'; } if (String(mediaType || '').trim() === 'qr-image') { return 'existing_region_qr_image_'; } return 'existing_region_image_'; } function getUploadZone(input) { if (typeof callbacks.getUploadZone === 'function') { return callbacks.getUploadZone(input); } return input ? input.closest('[data-region-upload-zone]') : null; } function getUploadProgressNode(zone) { return zone ? zone.querySelector('[data-region-upload-progress]') : null; } function getUploadProgressBar(zone) { return zone ? zone.querySelector('[data-region-upload-progress-bar]') : null; } function getUploadProgressText(zone) { return zone ? zone.querySelector('[data-region-upload-progress-text]') : null; } function setUploadProgress(zone, percent, text, isIndeterminate) { var progress = getUploadProgressNode(zone); var bar = getUploadProgressBar(zone); var progressText = getUploadProgressText(zone); var normalizedPercent = Math.max(0, Math.min(100, Math.round(Number(percent || 0)))); if (!progress || !bar) { return; } if (progressText) { progressText.textContent = text || 'Uploading...'; } progress.hidden = false; bar.style.width = (isIndeterminate ? 100 : normalizedPercent) + '%'; bar.textContent = isIndeterminate ? 'Uploading...' : normalizedPercent + '%'; bar.setAttribute('aria-valuenow', String(isIndeterminate ? 100 : normalizedPercent)); bar.classList.toggle('progress-bar-striped', Boolean(isIndeterminate)); bar.classList.toggle('progress-bar-animated', Boolean(isIndeterminate)); } function setUploadState(input, isUploading, percent, text, isIndeterminate) { var zone = getUploadZone(input); var progress = getUploadProgressNode(zone); if (!zone || !progress) { return; } zone.classList.toggle('is-uploading', Boolean(isUploading)); zone.setAttribute('aria-busy', isUploading ? 'true' : 'false'); input.disabled = Boolean(isUploading); if (isUploading) { setUploadProgress(zone, percent, text, isIndeterminate); return; } progress.hidden = true; if (input) { input.disabled = false; } } function setInputFile(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 queueUploadCleanup(uploadPaths) { var normalizedPaths = Array.from(new Set((uploadPaths || []).map(function (value) { return String(value || '').trim(); }).filter(Boolean))); if (!normalizedPaths.length) { return; } if (typeof callbacks.queueUploadCleanup === 'function') { callbacks.queueUploadCleanup(normalizedPaths); return; } var payload = JSON.stringify({ uploadPaths: normalizedPaths }); var url = String(callbacks.cleanupUrl || '/slides/uploads/cleanup'); if (navigator.sendBeacon) { try { navigator.sendBeacon(url, new Blob([payload], { type: 'application/json' })); return; } catch (_error) { // Fall back to fetch below. } } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: payload, credentials: 'same-origin', keepalive: true }).catch(function () { // Best-effort cleanup only. }); } function getPendingUploadCleanupPaths() { return Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"], input[type="file"][name^="region_qr_image_"]')).map(function (input) { if (!input || String(input.dataset.uploadedNeedsCleanup || '') !== '1') { return null; } return String(input.dataset.uploadedPath || '').trim(); }).filter(Boolean); } function clearPendingUploadCleanupPaths() { Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"], input[type="file"][name^="region_qr_image_"]')).forEach(function (input) { if (!input) { return; } delete input.dataset.uploadedNeedsCleanup; }); } function uploadFile(input, file) { if (!input || !file) { return Promise.resolve(null); } var mediaType = getMediaTypeFromInput(input); var regionId = input.name.replace(/^region_(?:image|video|qr_image)_/, ''); var card = getCard(regionId); var hidden = card && card.querySelector('input[type="hidden"][name="' + getExistingMediaPrefix(mediaType) + regionId + '"]'); var zone = getUploadZone(input); var uploadToken = String(Date.now()) + ':' + Math.random().toString(16).slice(2); var previousUploadedPath = String(input.dataset.uploadedPath || '').trim(); var previousNeedsCleanup = String(input.dataset.uploadedNeedsCleanup || '') === '1'; var uploadUrl = String(callbacks.uploadUrl || '/slides/uploads'); var uploadTimeoutMs = Math.max(1, Number(callbacks.uploadTimeoutMs || 30000)); delete input.dataset.uploadCancelRequested; input.dataset.uploadToken = uploadToken; setUploadState(input, true, 0, 'Uploading...', true); return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); var formData = new FormData(); formData.append('file', file, file.name || 'upload'); xhr.open('POST', uploadUrl, true); xhr.responseType = 'text'; xhr.withCredentials = true; xhr.timeout = uploadTimeoutMs; xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.setRequestHeader('Accept', 'application/json, text/plain, */*'); xhr.upload.onprogress = function (event) { if (input.dataset.uploadToken !== uploadToken) { return; } if (!event || !event.lengthComputable || !event.total) { setUploadProgress(zone, 100, 'Uploading...', true); return; } var percent = Math.round((event.loaded / event.total) * 100); setUploadProgress(zone, percent, 'Uploading ' + percent + '%', false); }; xhr.onload = function () { if (input.dataset.uploadToken !== uploadToken) { resolve(null); return; } var response = { ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText || '' }; if (!response.ok) { var error = new Error(response.responseText || 'Unable to upload media.'); error.status = response.status; error.responseText = response.responseText; reject(error); return; } if (String(response.responseText || '').trim().toLowerCase().indexOf('