// Time/date region rendering and live updates. var registry = window.pulsePlayerRegionTypes; var DEFAULT_FORMAT = '{{hh}}:{{mm}}'; var DEFAULT_STYLE = { font_family: 'Arial', font_size: 32, font_color: '#000000' }; var timeDateFormatterCache = Object.create(null); function escapeHtml(value) { return String(value === undefined || value === null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } 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 = []; 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 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 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, String(match[3] || '')) + '>'; }); } function getDefaultStyle() { return { font_family: DEFAULT_STYLE.font_family, font_size: DEFAULT_STYLE.font_size, font_color: DEFAULT_STYLE.font_color }; } function getDefaultTimeZone() { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch (_error) { return 'UTC'; } } function resolveTimeZone(value) { var raw = String(value || '').trim(); if (!raw) { return getDefaultTimeZone(); } try { new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date()); return raw; } catch (_error) { return getDefaultTimeZone(); } } function getFormatter(key, options) { if (!timeDateFormatterCache[key]) { timeDateFormatterCache[key] = new Intl.DateTimeFormat('en-GB', options); } return timeDateFormatterCache[key]; } function getFormattedParts(timeZone, date) { var targetDate = date instanceof Date ? date : new Date(); var resolvedTimeZone = resolveTimeZone(timeZone); var numericParts = getFormatter('numeric:' + resolvedTimeZone, { timeZone: resolvedTimeZone, hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric' }).formatToParts(targetDate); var weekdayLong = getFormatter('weekday-long:' + resolvedTimeZone, { timeZone: resolvedTimeZone, weekday: 'long' }).formatToParts(targetDate); var weekdayShort = getFormatter('weekday-short:' + resolvedTimeZone, { timeZone: resolvedTimeZone, weekday: 'short' }).formatToParts(targetDate); var monthLong = getFormatter('month-long:' + resolvedTimeZone, { timeZone: resolvedTimeZone, month: 'long' }).formatToParts(targetDate); var monthShort = getFormatter('month-short:' + resolvedTimeZone, { timeZone: resolvedTimeZone, month: 'short' }).formatToParts(targetDate); var ampm = getFormatter('ampm:' + resolvedTimeZone, { timeZone: resolvedTimeZone, hour12: true, hour: '2-digit', minute: '2-digit' }).formatToParts(targetDate); var timezoneShort = getFormatter('tz-short:' + resolvedTimeZone, { timeZone: resolvedTimeZone, timeZoneName: 'short' }).formatToParts(targetDate); function getPart(parts, type) { var match = parts.find(function (part) { return part && part.type === type; }); return match ? String(match.value || '') : ''; } function toTitleCase(value) { return String(value || '').toLowerCase().replace(/\b([a-z])/g, function (match, letter) { return String(letter || '').toUpperCase(); }); } return { h: String(Number(getPart(numericParts, 'hour')) || 0), hh: getPart(numericParts, 'hour'), m: String(Number(getPart(numericParts, 'minute')) || 0), mm: getPart(numericParts, 'minute'), s: String(Number(getPart(numericParts, 'second')) || 0), ss: getPart(numericParts, 'second'), d: String(Number(getPart(numericParts, 'day')) || 0), dd: getPart(numericParts, 'day'), M: String(Number(getPart(numericParts, 'month')) || 0), MM: getPart(numericParts, 'month'), y: String(Number(getPart(numericParts, 'year')) || 0), yyyy: getPart(numericParts, 'year'), yy: String(Number(String(getPart(numericParts, 'year')).slice(-2)) || 0).padStart(2, '0'), ddd: toTitleCase(getPart(weekdayShort, 'weekday')), dddd: toTitleCase(getPart(weekdayLong, 'weekday')), MMM: toTitleCase(getPart(monthShort, 'month')), MMMM: toTitleCase(getPart(monthLong, 'month')), a: toTitleCase(getPart(ampm, 'dayPeriod')), tz: resolvedTimeZone, tz_short: getPart(timezoneShort, 'timeZoneName'), date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'), time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second') }; } function resolveTimeDatePlaceholder(values, expression) { if (typeof placeholderUtils.resolvePlaceholderExpression === 'function' && typeof placeholderUtils.formatPlaceholderValue === 'function') { return placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(values, expression)); } var parsed = String(expression || '').trim(); return Object.prototype.hasOwnProperty.call(values, parsed) ? values[parsed] : ''; } function renderTemplate(format, timeZone, date) { var template = String(format || '').trim() || DEFAULT_FORMAT; var values = getFormattedParts(timeZone, date); return template.replace(/\{\{\s*([a-zA-Z0-9_.()\-]+)\s*\}\}/g, function (_match, key) { return String(resolveTimeDatePlaceholder(values, key) || ''); }); } function getTextStyle(regionContent, region) { var defaultStyle = getDefaultStyle(); return { font_family: regionContent.font_family || region.fontFamily || defaultStyle.font_family, font_size: regionContent.font_size || region.fontSize || defaultStyle.font_size, font_color: regionContent.font_color || region.fontColor || defaultStyle.font_color }; } function renderTimeDateRegion(region, regionContent) { var content = regionContent && typeof regionContent === 'object' ? regionContent : {}; var format = String(content.value !== undefined ? content.value : content.text || '').trim() || DEFAULT_FORMAT; var timeZone = resolveTimeZone(content.timezone || content.time_zone || ''); var style = getTextStyle(content, region); var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : ''; var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : ''; var fontColor = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : ''; var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? fontFamily : '') + fontSize + fontColor + 'white-space:pre-wrap;line-height:1.1;'; var renderedText = renderTemplate(format, timeZone, new Date()); return '
' + renderEditorJsContent(renderedText) + '
'; } function updateTimeDateRegion(element) { if (!element) { return; } var format = String(element.dataset.timeDateFormat || '').trim() || DEFAULT_FORMAT; var timeZone = String(element.dataset.timeDateTimezone || '').trim(); var scaleWrapper = element.querySelector('.template-region-text-scale'); if (!scaleWrapper) { return; } scaleWrapper.innerHTML = renderEditorJsContent(renderTemplate(format, timeZone, new Date())); } function scheduleTimeDateRegionUpdate(element) { if (!element) { return; } if (element.__timeDateTimer) { window.clearTimeout(element.__timeDateTimer); element.__timeDateTimer = null; } function tick() { if (!element.isConnected) { if (element.__timeDateTimer) { window.clearTimeout(element.__timeDateTimer); element.__timeDateTimer = null; } return; } updateTimeDateRegion(element); element.__timeDateTimer = window.setTimeout(tick, Math.max(100, 1000 - (Date.now() % 1000))); } tick(); } function destroyTimeDateRegions(root) { if (!root) { return; } Array.prototype.forEach.call(root.querySelectorAll('.template-region.time-date'), function (element) { if (element && element.__timeDateTimer) { window.clearTimeout(element.__timeDateTimer); element.__timeDateTimer = null; } }); } function initializeTimeDateRegions(root) { if (!root) { return; } Array.prototype.forEach.call(root.querySelectorAll('.template-region.time-date'), function (element) { scheduleTimeDateRegionUpdate(element); }); } registry.register('time-date', { label: 'Time / Date', getDefaultStyle: getDefaultStyle, getDefaultRegionSize: function () { return { width: 420, height: 180 }; }, renderRegion: renderTimeDateRegion, initRegion: initializeTimeDateRegions, destroyRegion: destroyTimeDateRegions });