Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m18s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 33s
957 lines
31 KiB
JavaScript
957 lines
31 KiB
JavaScript
// 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, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function sanitizePreviewHtml(html) {
|
|
var output = String(html || '');
|
|
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
output = output.replace(/<style[\s\S]*?<\/style>/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', 'col', 'colgroup', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
|
|
if (allowed.indexOf(name) === -1) {
|
|
return '';
|
|
}
|
|
|
|
if (name === 'img') {
|
|
var srcMatch = String(attrText || '').match(/\bsrc\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+))/i);
|
|
var srcValue = srcMatch ? String(srcMatch[2] !== undefined ? srcMatch[2] : srcMatch[3] !== undefined ? srcMatch[3] : srcMatch[4] !== undefined ? srcMatch[4] : '').trim() : '';
|
|
if (!srcValue || !/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/]|data:image\/)/i.test(srcValue)) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
if (closing) {
|
|
return '</' + name + '>';
|
|
}
|
|
|
|
return '<' + name + sanitizeTagAttributes(name, attrText) + '>';
|
|
});
|
|
}
|
|
|
|
function sanitizeRichText(html) {
|
|
var output = String(html || '');
|
|
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
output = output.replace(/<style[\s\S]*?<\/style>/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 allowed = ['a', 'b', 'blockquote', 'br', 'code', 'col', 'colgroup', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', '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 '</' + name + '>';
|
|
}
|
|
|
|
return '<' + name + sanitizeRichTextAttributes(name, attrText) + '>';
|
|
});
|
|
}
|
|
|
|
function sanitizeRichTextAttributes(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'],
|
|
img: ['src', 'alt', 'title', 'width', 'height', 'class', 'style', 'loading', 'decoding'],
|
|
col: ['class', 'style', 'span', 'width'],
|
|
colgroup: ['class', 'style', 'span'],
|
|
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 '';
|
|
}
|
|
|
|
if (tagName === 'img') {
|
|
var srcMatch = String(attrText || '').match(/\bsrc\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+))/i);
|
|
var srcValue = srcMatch ? String(srcMatch[2] !== undefined ? srcMatch[2] : srcMatch[3] !== undefined ? srcMatch[3] : srcMatch[4] !== undefined ? srcMatch[4] : '').trim() : '';
|
|
if (!srcValue || !/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/]|data:image\/)/i.test(srcValue)) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
var attrs = [];
|
|
String(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 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'],
|
|
img: ['src', 'alt', 'title', 'width', 'height', 'class', 'style', 'loading', 'decoding'],
|
|
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('<!doctype html') === 0 || String(response.responseText || '').indexOf('<html') !== -1) {
|
|
reject(new Error('Upload redirected to an HTML page. Please sign in again and retry.'));
|
|
return;
|
|
}
|
|
|
|
var payload = {};
|
|
try {
|
|
payload = JSON.parse(response.responseText || '{}') || {};
|
|
} catch (_error) {
|
|
reject(new Error('Unable to parse the upload response.'));
|
|
return;
|
|
}
|
|
|
|
var uploadedPath = String(payload.path || '').trim();
|
|
if (!uploadedPath) {
|
|
reject(new Error('Unable to upload media.'));
|
|
return;
|
|
}
|
|
|
|
if (input.dataset.previewUrl) {
|
|
URL.revokeObjectURL(input.dataset.previewUrl);
|
|
delete input.dataset.previewUrl;
|
|
}
|
|
|
|
if (String(input.dataset.uploadCancelRequested || '') === '1') {
|
|
if (uploadedPath) {
|
|
queueUploadCleanup([uploadedPath]);
|
|
}
|
|
|
|
setUploadState(input, false);
|
|
resolve(payload);
|
|
return;
|
|
}
|
|
|
|
if (hidden) {
|
|
hidden.value = uploadedPath;
|
|
hidden.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
input.dataset.uploadedPath = uploadedPath;
|
|
input.dataset.uploadedNeedsCleanup = '1';
|
|
input.value = '';
|
|
input.setCustomValidity('');
|
|
|
|
if (typeof callbacks.renderPreview === 'function') {
|
|
callbacks.renderPreview(card, uploadedPath);
|
|
}
|
|
|
|
if (typeof callbacks.syncDuration === 'function') {
|
|
callbacks.syncDuration(card, uploadedPath);
|
|
}
|
|
|
|
if (typeof callbacks.markEdited === 'function') {
|
|
callbacks.markEdited();
|
|
}
|
|
|
|
if (typeof callbacks.requestPreviewRender === 'function') {
|
|
callbacks.requestPreviewRender();
|
|
}
|
|
|
|
if (previousNeedsCleanup && previousUploadedPath && previousUploadedPath !== uploadedPath) {
|
|
queueUploadCleanup([previousUploadedPath]);
|
|
}
|
|
|
|
setUploadState(input, false);
|
|
resolve(payload);
|
|
};
|
|
|
|
xhr.onerror = function () {
|
|
if (input.dataset.uploadToken !== uploadToken) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
reject(new Error('Unable to upload media.'));
|
|
};
|
|
|
|
xhr.onabort = function () {
|
|
if (input.dataset.uploadToken !== uploadToken) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
reject(new Error('Unable to upload media.'));
|
|
};
|
|
|
|
xhr.ontimeout = function () {
|
|
if (input.dataset.uploadToken !== uploadToken) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
reject(new Error('Upload timed out. Please try again.'));
|
|
};
|
|
|
|
xhr.onloadend = function () {
|
|
if (input.dataset.uploadToken === uploadToken) {
|
|
delete input.dataset.uploadToken;
|
|
delete input.dataset.uploadCancelRequested;
|
|
setUploadState(input, false);
|
|
}
|
|
};
|
|
|
|
xhr.send(formData);
|
|
}).catch(function (error) {
|
|
if (input.dataset.uploadToken === uploadToken) {
|
|
delete input.dataset.uploadToken;
|
|
setUploadState(input, false);
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
function handleSelection(input, file) {
|
|
if (typeof callbacks.handleMediaSelection !== 'function') {
|
|
return false;
|
|
}
|
|
|
|
return callbacks.handleMediaSelection(input, file, {
|
|
setInputFile: setInputFile,
|
|
uploadFile: uploadFile,
|
|
getMediaTypeFromInput: getMediaTypeFromInput,
|
|
getExistingMediaPrefix: getExistingMediaPrefix,
|
|
getMediaPrefix: getMediaPrefix,
|
|
getUploadZone: getUploadZone,
|
|
queueUploadCleanup: queueUploadCleanup,
|
|
getPendingUploadCleanupPaths: getPendingUploadCleanupPaths,
|
|
clearPendingUploadCleanupPaths: clearPendingUploadCleanupPaths
|
|
});
|
|
}
|
|
|
|
if (!templateFields.dataset.regionMediaUploadBound) {
|
|
templateFields.dataset.regionMediaUploadBound = '1';
|
|
|
|
templateFields.addEventListener('change', function (event) {
|
|
var input = event.target && event.target.matches && event.target.matches('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"], input[type="file"][name^="region_qr_image_"]') ? event.target : null;
|
|
|
|
if (!input || !templateFields.contains(input)) {
|
|
return;
|
|
}
|
|
|
|
var file = input.files && input.files.length ? input.files[0] : null;
|
|
var hookResult = handleSelection(input, file);
|
|
|
|
if (hookResult === false) {
|
|
return;
|
|
}
|
|
|
|
if (hookResult && typeof hookResult.then === 'function') {
|
|
hookResult.catch(function (error) {
|
|
if (input.dataset.previewUrl) {
|
|
URL.revokeObjectURL(input.dataset.previewUrl);
|
|
delete input.dataset.previewUrl;
|
|
}
|
|
input.value = '';
|
|
if (typeof callbacks.onUploadError === 'function') {
|
|
callbacks.onUploadError(error, input);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (file) {
|
|
uploadFile(input, file).catch(function (error) {
|
|
if (input.dataset.previewUrl) {
|
|
URL.revokeObjectURL(input.dataset.previewUrl);
|
|
delete input.dataset.previewUrl;
|
|
}
|
|
input.value = '';
|
|
if (typeof callbacks.onUploadError === 'function') {
|
|
callbacks.onUploadError(error, input);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
templateFields.addEventListener('dragover', function (event) {
|
|
var zone = event.target && event.target.closest ? event.target.closest('[data-region-upload-zone]') : null;
|
|
if (!zone || !templateFields.contains(zone)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
zone.classList.add('is-dragover');
|
|
});
|
|
|
|
templateFields.addEventListener('dragleave', function (event) {
|
|
var zone = event.target && event.target.closest ? event.target.closest('[data-region-upload-zone]') : null;
|
|
if (!zone || !templateFields.contains(zone)) {
|
|
return;
|
|
}
|
|
|
|
zone.classList.remove('is-dragover');
|
|
});
|
|
|
|
templateFields.addEventListener('drop', function (event) {
|
|
var zone = event.target && event.target.closest ? event.target.closest('[data-region-upload-zone]') : null;
|
|
|
|
if (!zone || !templateFields.contains(zone)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
zone.classList.remove('is-dragover');
|
|
|
|
var input = zone.querySelector('input[type="file"]');
|
|
var files = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files : null;
|
|
var file = files && files.length ? files[0] : null;
|
|
|
|
if (!input || !file) {
|
|
return;
|
|
}
|
|
|
|
var hookResult = handleSelection(input, file);
|
|
|
|
if (hookResult === false) {
|
|
return;
|
|
}
|
|
|
|
if (hookResult && typeof hookResult.then === 'function') {
|
|
hookResult.catch(function (error) {
|
|
if (input.dataset.previewUrl) {
|
|
URL.revokeObjectURL(input.dataset.previewUrl);
|
|
delete input.dataset.previewUrl;
|
|
}
|
|
input.value = '';
|
|
if (typeof callbacks.onUploadError === 'function') {
|
|
callbacks.onUploadError(error, input);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
setInputFile(input, file);
|
|
});
|
|
}
|
|
|
|
return {
|
|
uploadFile: uploadFile,
|
|
queueUploadCleanup: queueUploadCleanup,
|
|
getPendingUploadCleanupPaths: getPendingUploadCleanupPaths,
|
|
clearPendingUploadCleanupPaths: clearPendingUploadCleanupPaths,
|
|
setInputFile: setInputFile
|
|
};
|
|
}
|
|
|
|
window.pulseRegionUtils = {
|
|
escapeHtml: escapeHtml,
|
|
sanitizePreviewHtml: sanitizePreviewHtml,
|
|
sanitizeRichText: sanitizeRichText,
|
|
sanitizeRichTextAttributes: sanitizeRichTextAttributes,
|
|
sanitizeFontFamily: sanitizeFontFamily,
|
|
sanitizeTextColor: sanitizeTextColor,
|
|
normalizeAcceptList: normalizeAcceptList,
|
|
fileMatchesAccept: fileMatchesAccept,
|
|
bindRegionMediaRemoveControls: bindRegionMediaRemoveControls,
|
|
createRegionMediaUploadController: createRegionMediaUploadController
|
|
};
|
|
}()); |