Files
pulse-signage/src/web/public/js/shared/placeholder-utils.js
T
lzstealth 98f969ca0f
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m47s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s
Release v2.11.1
2026-09-04 15:43:55 +01:00

594 lines
20 KiB
JavaScript

// Shared placeholder parsing and resolution helpers for browser-rendered API content.
(function () {
var root = window;
var transformPattern = /^([a-z_][a-z0-9_]*)\((.*)\)$/i;
var monthNamesShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var monthNamesLong = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var dayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
var dayNamesLong = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
function resolvePath(value, path) {
var current = value;
if (!path) {
return current;
}
String(path).split('.').forEach(function (segment) {
if (current === undefined || current === null) {
current = '';
return;
}
current = current[segment];
});
return current === undefined || current === null ? '' : current;
}
function splitExpressionSegments(value) {
var segments = [];
var current = '';
var depth = 0;
String(value || '').split('').forEach(function (character) {
if (character === '(') {
depth += 1;
} else if (character === ')' && depth > 0) {
depth -= 1;
}
if (character === '.' && depth === 0) {
segments.push(current);
current = '';
return;
}
current += character;
});
segments.push(current);
return segments;
}
function parsePlaceholderExpression(expression) {
var raw = String(expression || '').trim();
var segments = raw ? splitExpressionSegments(raw) : [];
var transforms = [];
while (segments.length) {
var candidate = String(segments[segments.length - 1] || '').trim();
var match = candidate.match(transformPattern);
if (!match) {
break;
}
transforms.unshift({
name: String(match[1] || '').trim().toLowerCase(),
args: match[2] ? splitTransformArgs(match[2]) : []
});
segments.pop();
}
return {
path: segments.join('.'),
transforms: transforms
};
}
function splitTransformArgs(value) {
var source = String(value || '').trim();
if (!source) {
return [];
}
var args = [];
var current = '';
var quote = '';
source.split('').forEach(function (character) {
if ((character === '"' || character === '\'') && (!quote || quote === character)) {
quote = quote ? '' : character;
current += character;
return;
}
if (character === ',' && !quote) {
if (current.trim()) {
args.push(current.trim());
}
current = '';
return;
}
current += character;
});
if (current.trim()) {
args.push(current.trim());
}
return args.map(function (item) {
var normalized = String(item || '').trim();
if ((normalized[0] === '"' && normalized[normalized.length - 1] === '"') || (normalized[0] === '\'' && normalized[normalized.length - 1] === '\'')) {
return normalized.slice(1, -1);
}
return normalized;
}).filter(Boolean);
}
function padNumber(value, size) {
var text = String(Math.abs(Number(value || 0)));
while (text.length < size) {
text = '0' + text;
}
return text;
}
function getDefaultTimeZone() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
} catch (_error) {
return 'UTC';
}
}
function getDateValue(value) {
return value instanceof Date ? value : new Date(value);
}
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 getDatePartsFromLocalTime(date) {
var dayPeriod = date.getHours() >= 12 ? 'PM' : 'AM';
return {
year: String(date.getFullYear()),
month: padNumber(date.getMonth() + 1, 2),
day: padNumber(date.getDate(), 2),
hour24: padNumber(date.getHours(), 2),
hour12: padNumber(date.getHours() % 12 || 12, 2),
minute: padNumber(date.getMinutes(), 2),
second: padNumber(date.getSeconds(), 2),
weekdayLong: dayNamesLong[date.getDay()],
weekdayShort: dayNamesShort[date.getDay()],
monthLong: monthNamesLong[date.getMonth()],
monthShort: monthNamesShort[date.getMonth()],
dayPeriod: dayPeriod
};
}
function getDatePartsFromTimeZone(date, timeZone) {
var resolvedTimeZone = resolveTimeZone(timeZone);
var baseFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
var weekdayLongFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
weekday: 'long'
});
var weekdayShortFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
weekday: 'short'
});
var monthLongFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
month: 'long'
});
var monthShortFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
month: 'short'
});
var ampmFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
hour12: true,
hour: '2-digit',
minute: '2-digit'
});
var numericParts = baseFormatter.formatToParts(date);
var weekdayLong = weekdayLongFormatter.formatToParts(date);
var weekdayShort = weekdayShortFormatter.formatToParts(date);
var monthLong = monthLongFormatter.formatToParts(date);
var monthShort = monthShortFormatter.formatToParts(date);
var ampm = ampmFormatter.formatToParts(date);
function getPart(parts, type) {
var match = parts.find(function (part) {
return part && part.type === type;
});
return match ? String(match.value || '') : '';
}
var dayPeriod = getPart(ampm, 'dayPeriod');
return {
year: getPart(numericParts, 'year'),
month: getPart(numericParts, 'month'),
day: getPart(numericParts, 'day'),
hour24: getPart(numericParts, 'hour'),
hour12: getPart(ampm, 'hour'),
minute: getPart(numericParts, 'minute'),
second: getPart(numericParts, 'second'),
weekdayLong: getPart(weekdayLong, 'weekday'),
weekdayShort: getPart(weekdayShort, 'weekday'),
monthLong: getPart(monthLong, 'month'),
monthShort: getPart(monthShort, 'month'),
dayPeriod: String(dayPeriod || '').toUpperCase(),
timeZone: resolvedTimeZone
};
}
function formatDateValue(value, pattern, timeZone) {
var date = getDateValue(value);
if (Number.isNaN(date.getTime())) {
return '';
}
var format = String(pattern || 'YYYY-MM-DD HH:mm').trim() || 'YYYY-MM-DD HH:mm';
var parts = timeZone ? getDatePartsFromTimeZone(date, timeZone) : getDatePartsFromLocalTime(date);
var hours24 = parts.hour24;
var hours12 = parts.hour12;
var tokenMap = {
YYYY: parts.year,
YY: parts.year.slice(-2),
MMMM: parts.monthLong,
MMM: parts.monthShort,
MM: parts.month,
M: String(Number(parts.month) || 0),
DD: parts.day,
D: String(Number(parts.day) || 0),
dddd: parts.weekdayLong,
ddd: parts.weekdayShort,
HH: hours24,
H: String(Number(hours24) || 0),
hh: hours12,
h: String(Number(hours12) || 0),
mm: parts.minute,
m: String(Number(parts.minute) || 0),
ss: parts.second,
s: String(Number(parts.second) || 0),
A: parts.dayPeriod,
a: String(parts.dayPeriod || '').toLowerCase()
};
return format.replace(/\[([^\]]+)\]|YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g, function (match, literal) {
return literal || tokenMap[match] || match;
});
}
function getTimeZoneShortName(value, timeZone) {
var date = getDateValue(value);
if (Number.isNaN(date.getTime())) {
return '';
}
var resolvedTimeZone = resolveTimeZone(timeZone);
try {
var parts = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
timeZoneName: 'short'
}).formatToParts(date);
var match = parts.find(function (part) {
return part && part.type === 'timeZoneName';
});
return match ? String(match.value || '') : resolvedTimeZone;
} catch (_error) {
return resolvedTimeZone;
}
}
function getTimeZoneLongName(timeZone) {
return getTransformTimeZone([], { timeZone: timeZone });
}
function getTransformTimeZone(args, options) {
return resolveTimeZone((args && args[0]) || (options && options.timeZone) || '');
}
function toNumericValue(value) {
if (value && typeof value === 'object') {
var numericKeys = ['value', 'amount', 'current', 'total', 'goal', 'raised'];
for (var keyIndex = 0; keyIndex < numericKeys.length; keyIndex += 1) {
var nestedValue = value[numericKeys[keyIndex]];
if (nestedValue !== undefined && nestedValue !== null && nestedValue !== value) {
var nestedNumber = toNumericValue(nestedValue);
if (Number.isFinite(nestedNumber)) {
return nestedNumber;
}
}
}
return NaN;
}
var normalized = String(value === undefined || value === null ? '' : value).replace(/[^0-9.eE+-]/g, '');
var number = Number(normalized);
return Number.isFinite(number) ? number : NaN;
}
function applyTransform(value, transform, options) {
var text = String(value === undefined || value === null ? '' : value);
var name = String(transform && transform.name || '').trim().toLowerCase();
var args = Array.isArray(transform && transform.args) ? transform.args : [];
if (name === 'lower') {
return text.toLowerCase();
}
if (name === 'upper') {
return text.toUpperCase();
}
if (name === 'title') {
return text.toLowerCase().replace(/\b([a-z])/g, function (match, letter) {
return String(letter || '').toUpperCase();
});
}
if (name === 'format' || name === 'date' || name === 'datetime' || name === 'time') {
return formatDateValue(value, args[0] || (name === 'time' ? 'h:mm A' : name === 'date' ? 'MMM D, YYYY' : 'MMM D, YYYY h:mm A'), getTransformTimeZone(args.slice(1), options));
}
if (name === 'tz') {
return getTimeZoneShortName(value, getTransformTimeZone(args, options));
}
if (name === 'tz_long') {
return getTimeZoneLongName(getTransformTimeZone(args, options));
}
if (name === 'add' || name === 'subtract' || name === 'multiply' || name === 'divide') {
var arithmeticValue = toNumericValue(value);
var operand = toNumericValue(args[0]);
if (!Number.isFinite(arithmeticValue) || !Number.isFinite(operand) || (name === 'divide' && operand === 0)) {
return value;
}
if (name === 'add') return arithmeticValue + operand;
if (name === 'subtract') return arithmeticValue - operand;
if (name === 'multiply') return arithmeticValue * operand;
return arithmeticValue / operand;
}
return text;
}
function resolvePlaceholderExpression(value, expression, options) {
var parsed = parsePlaceholderExpression(expression);
var resolved = resolvePath(value, parsed.path);
parsed.transforms.forEach(function (transform) {
resolved = applyTransform(resolved, transform, options || {});
});
return resolved;
}
function isImagePlaceholderExpression(expression) {
var parsed = parsePlaceholderExpression(expression);
return parsed.transforms.some(function (transform) {
return transform && transform.name === 'image';
});
}
function getImagePlaceholderConfig(expression) {
var parsed = parsePlaceholderExpression(expression);
var imageTransform = parsed.transforms.find(function (transform) {
return transform && transform.name === 'image';
});
var args = imageTransform && Array.isArray(imageTransform.args) ? imageTransform.args : [];
var width = Number(args[0]);
var height = Number(args[1]);
return {
width: Number.isFinite(width) && width > 0 ? Math.round(width) : 0,
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 0
};
}
function isProgressPlaceholderExpression(expression) {
var parsed = parsePlaceholderExpression(expression);
return parsed.transforms.some(function (transform) {
return transform && transform.name === 'progress';
});
}
function renderProgressPlaceholder(value, expression) {
var parsed = parsePlaceholderExpression(expression);
var transform = parsed.transforms.find(function (candidate) {
return candidate && candidate.name === 'progress';
});
if (!transform || !value || typeof value !== 'object' || transform.args.length < 2) {
return '';
}
function toNumber(raw) {
if (raw && typeof raw === 'object') {
var numericKeys = ['value', 'amount', 'current', 'total', 'goal', 'raised'];
for (var keyIndex = 0; keyIndex < numericKeys.length; keyIndex += 1) {
var nestedValue = raw[numericKeys[keyIndex]];
if (nestedValue !== undefined && nestedValue !== null && nestedValue !== raw) {
var nestedNumber = toNumber(nestedValue);
if (Number.isFinite(nestedNumber)) {
return nestedNumber;
}
}
}
return 0;
}
var normalized = String(raw === undefined || raw === null ? '' : raw).replace(/[^0-9.eE+-]/g, '');
var number = Number(normalized);
return Number.isFinite(number) ? number : 0;
}
function resolveProgressValue(argument) {
var raw = String(argument === undefined || argument === null ? '' : argument).trim();
var literal = raw.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2');
if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(literal)) {
return literal;
}
return resolvePath(value, raw);
}
var startValue = resolveProgressValue(transform.args[0]);
var endValue = resolveProgressValue(transform.args[1]);
var startDate = startValue instanceof Date ? startValue : new Date(startValue);
var endDate = endValue instanceof Date ? endValue : new Date(endValue);
var percentage;
if (typeof startValue === 'string' && typeof endValue === 'string' && /[-T]/.test(startValue) && /[-T]/.test(endValue) && !Number.isNaN(startDate.getTime()) && !Number.isNaN(endDate.getTime()) && endDate.getTime() > startDate.getTime()) {
percentage = Math.max(0, Math.min(100, ((Date.now() - startDate.getTime()) / (endDate.getTime() - startDate.getTime())) * 100));
} else {
var current = toNumber(startValue);
var goal = toNumber(endValue);
percentage = goal > 0 ? Math.max(0, Math.min(100, (current / goal) * 100)) : 0;
}
var roundedPercentage = Math.round(percentage * 10) / 10;
var label = roundedPercentage + '%';
var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'];
var variantColors = {
primary: '#0d6efd',
secondary: '#6c757d',
success: '#198754',
danger: '#dc3545',
warning: '#ffc107',
info: '#0dcaf0',
light: '#f8f9fa',
dark: '#212529'
};
var announcementColors = {
orange: '#c84e10',
amber: '#a56710',
olive: '#5f7f0f',
teal: '#12827d',
sky: '#127caf',
indigo: '#6f60ea',
violet: '#9553db',
fuchsia: '#b347be',
pink: '#cd388d',
navy: '#1d2d4c',
steel: '#3a4860',
slate: '#566577',
graphite: '#32363c',
midnight: '#1e1d2d'
};
var variant = 'primary';
var barVariant = '';
var customColor = '';
var backgroundColor = '';
var colorCount = 0;
var modifiers = [];
var textless = false;
var borderRadius = 'var(--bs-border-radius)';
transform.args.slice(2).forEach(function (argument) {
var option = String(argument || '').trim().toLowerCase();
var isNamedColor = variants.indexOf(option) !== -1 || Object.prototype.hasOwnProperty.call(announcementColors, option);
var isHexColor = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(option);
if (isNamedColor || isHexColor) {
var color = isHexColor ? option : (variantColors[option] || announcementColors[option]);
if (colorCount === 0) {
if (variants.indexOf(option) !== -1) {
variant = option;
barVariant = option;
}
customColor = color;
} else if (colorCount === 1) {
backgroundColor = color;
}
colorCount += 1;
}
if (option === 'striped' || option === 'animated') {
modifiers.push('progress-bar-' + option);
}
if (option === 'textless') {
textless = true;
}
if (option === 'square') {
borderRadius = '0';
} else if (option === 'pill') {
borderRadius = '50rem';
} else if (option === 'rounded') {
borderRadius = 'var(--bs-border-radius)';
} else {
var radiusMatch = option.match(/^radius\((0|[0-9]+(?:\.[0-9]+)?(?:px|rem|em|%)?)\)$/);
if (radiusMatch) {
borderRadius = radiusMatch[1];
}
}
});
var progressClass = 'progress';
var barClass = 'progress-bar' + (barVariant ? ' bg-' + barVariant : '') + (modifiers.length ? ' ' + modifiers.join(' ') : '');
var barStyle = 'width:' + roundedPercentage + '%;color:inherit;';
if (customColor) {
barStyle += 'background-color:' + customColor + ';';
}
var progressStyleValue = (backgroundColor ? 'background-color:' + backgroundColor + ';' : '') + 'font-size:inherit;--bs-progress-font-size:inherit;--bs-progress-height:1em;height:1em;border-radius:' + borderRadius + ';';
var progressStyle = progressStyleValue ? ' style="' + progressStyleValue + '"' : '';
return '<span class="' + progressClass + ' api-progress"' + progressStyle + ' role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + roundedPercentage + '" aria-label="' + label + '">' +
'<span class="' + barClass + '" style="' + barStyle + '">' + (textless ? '' : label) + '</span></span>';
}
function formatPlaceholderValue(value) {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch (_error) {
return '';
}
}
return String(value);
}
function collectPlaceholderFieldPaths(value) {
var output = [];
function walk(currentValue, prefix) {
if (!currentValue || typeof currentValue !== 'object') {
return;
}
if (Array.isArray(currentValue)) {
return;
}
Object.keys(currentValue).forEach(function (key) {
var nextPath = prefix ? prefix + '.' + key : key;
var nextValue = currentValue[key];
if (nextValue && typeof nextValue === 'object') {
walk(nextValue, nextPath);
return;
}
if (output.indexOf(nextPath) === -1) {
output.push(nextPath);
}
});
}
walk(value, '');
return output;
}
root.placeholderUtils = {
resolvePath: resolvePath,
parsePlaceholderExpression: parsePlaceholderExpression,
resolvePlaceholderExpression: resolvePlaceholderExpression,
isImagePlaceholderExpression: isImagePlaceholderExpression,
getImagePlaceholderConfig: getImagePlaceholderConfig,
isProgressPlaceholderExpression: isProgressPlaceholderExpression,
renderProgressPlaceholder: renderProgressPlaceholder,
formatPlaceholderValue: formatPlaceholderValue,
collectPlaceholderFieldPaths: collectPlaceholderFieldPaths
};
}());