Release v2.11.1
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
(function () {
|
||||
var currentPasswordInput = document.querySelector('[data-account-current-password]');
|
||||
var profileFormInput = document.querySelector('[data-account-profile-current-password]');
|
||||
var passwordFormInput = document.querySelector('[data-account-password-current-password]');
|
||||
var profileForm = profileFormInput && profileFormInput.form;
|
||||
var passwordForm = passwordFormInput && passwordFormInput.form;
|
||||
|
||||
if (!currentPasswordInput || !profileFormInput || !passwordFormInput || !profileForm || !passwordForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
function syncCurrentPassword() {
|
||||
profileFormInput.value = currentPasswordInput.value;
|
||||
passwordFormInput.value = currentPasswordInput.value;
|
||||
}
|
||||
|
||||
function clearCurrentPassword() {
|
||||
currentPasswordInput.value = '';
|
||||
currentPasswordInput.setCustomValidity('');
|
||||
currentPasswordInput.removeAttribute('required');
|
||||
profileFormInput.value = '';
|
||||
passwordFormInput.value = '';
|
||||
passwordForm.querySelectorAll('input[type="password"]').forEach(function (input) {
|
||||
input.value = '';
|
||||
input.setCustomValidity('');
|
||||
});
|
||||
profileForm.classList.remove('was-validated');
|
||||
passwordForm.classList.remove('was-validated');
|
||||
}
|
||||
|
||||
currentPasswordInput.addEventListener('input', syncCurrentPassword);
|
||||
currentPasswordInput.addEventListener('input', function () {
|
||||
currentPasswordInput.setCustomValidity('');
|
||||
});
|
||||
document.addEventListener('submit', function (event) {
|
||||
if (event.target !== profileForm && event.target !== passwordForm) {
|
||||
return;
|
||||
}
|
||||
if (!currentPasswordInput.value) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
currentPasswordInput.setCustomValidity('Current password is required.');
|
||||
currentPasswordInput.reportValidity();
|
||||
return;
|
||||
}
|
||||
currentPasswordInput.setCustomValidity('');
|
||||
syncCurrentPassword();
|
||||
}, true);
|
||||
passwordForm.addEventListener('submit', syncCurrentPassword);
|
||||
document.addEventListener('web-async-save:success', function (event) {
|
||||
if (event.detail && event.detail.form !== profileForm && event.detail.form !== passwordForm) {
|
||||
return;
|
||||
}
|
||||
clearCurrentPassword();
|
||||
});
|
||||
})();
|
||||
@@ -72,9 +72,13 @@
|
||||
var allBlackout = hasClients && clients.every(function (client) {
|
||||
return Boolean(client && client.blackout);
|
||||
});
|
||||
var isClientsMobile = Boolean(document.querySelector('.clients-screen-command-card'));
|
||||
var useShortMobileLabels = isClientsMobile && window.innerWidth < 768;
|
||||
|
||||
if (action === 'pause') {
|
||||
var pauseLabel = allPaused ? 'Resume ' + (isAllScreens ? 'all clients' : 'screen') : 'Pause ' + (isAllScreens ? 'all clients' : 'screen');
|
||||
var pauseLabel = useShortMobileLabels
|
||||
? (allPaused ? 'Resume' : 'Pause')
|
||||
: (allPaused ? 'Resume ' + (isAllScreens ? 'all clients' : 'screen') : 'Pause ' + (isAllScreens ? 'all clients' : 'screen'));
|
||||
var pauseConfirm = allPaused ? 'Resume ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?' : 'Pause ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?';
|
||||
var pauseIcon = allPaused ? 'bi-play-fill' : 'bi-pause-fill';
|
||||
button.innerHTML = '<i class="bi ' + pauseIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(pauseLabel);
|
||||
@@ -92,7 +96,9 @@
|
||||
}
|
||||
|
||||
if (action === 'blackout') {
|
||||
var blackoutLabel = allBlackout ? 'Restore ' + (isAllScreens ? 'all clients' : 'screen') : 'Blackout ' + (isAllScreens ? 'all clients' : 'screen');
|
||||
var blackoutLabel = useShortMobileLabels
|
||||
? (allBlackout ? 'Restore' : 'Blackout')
|
||||
: (allBlackout ? 'Restore ' + (isAllScreens ? 'all clients' : 'screen') : 'Blackout ' + (isAllScreens ? 'all clients' : 'screen'));
|
||||
var blackoutConfirm = allBlackout ? 'Restore ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?' : 'Blackout ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?';
|
||||
var blackoutIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash';
|
||||
button.innerHTML = '<i class="bi ' + blackoutIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(blackoutLabel);
|
||||
@@ -147,6 +153,9 @@
|
||||
}
|
||||
|
||||
if (button) {
|
||||
if (Boolean(document.querySelector('.clients-screen-command-card')) && window.innerWidth < 768) {
|
||||
button.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload';
|
||||
}
|
||||
button.setAttribute('aria-label', selectedName ? selectedName : 'Selected screen group');
|
||||
}
|
||||
});
|
||||
@@ -372,7 +381,7 @@
|
||||
var connectionId = String(row.getAttribute('data-client-client-id') || row.getAttribute('data-client-id') || '').trim();
|
||||
var clientId = String(row.getAttribute('data-client-client-id') || '').trim();
|
||||
var playerBaseUrl = String(row.getAttribute('data-client-player-base-url') || '').trim();
|
||||
var clientNameCell = row.querySelector('td[data-label="Client"] > div');
|
||||
var clientNameCell = row.querySelector('td[data-label="Client"] > div, [data-mobile-client-name]');
|
||||
var clientName = String(clientNameCell && clientNameCell.textContent || '').trim();
|
||||
var options = Array.prototype.slice.call(elements.targetSelect.options || []);
|
||||
|
||||
@@ -826,7 +835,7 @@
|
||||
if (typeof elements.modal.addEventListener === 'function') {
|
||||
elements.modal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event && event.relatedTarget ? event.relatedTarget : null;
|
||||
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key]') : null;
|
||||
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key], article[data-mobile-client-key]') : null;
|
||||
if (row) {
|
||||
updateClientMoveModalFromRow(row);
|
||||
}
|
||||
@@ -843,7 +852,7 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null;
|
||||
var row = moveButton.closest ? moveButton.closest('tr[data-client-key], article[data-mobile-client-key]') : null;
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
@@ -1038,6 +1047,60 @@
|
||||
blackoutButton.setAttribute('aria-label', label);
|
||||
}
|
||||
|
||||
function updateMobileClientCards(state) {
|
||||
document.querySelectorAll('[data-mobile-client-id]').forEach(function (card) {
|
||||
var id = String(card.getAttribute('data-mobile-client-id') || '').trim();
|
||||
var clientId = String(card.getAttribute('data-mobile-client-client-id') || '').trim();
|
||||
var client = (state.clients || []).find(function (candidate) {
|
||||
return String(candidate.id || '').trim() === id || String(candidate.clientId || '').trim() === clientId;
|
||||
});
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
var paused = Boolean(client.paused);
|
||||
var blackout = Boolean(client.blackout);
|
||||
var badge = card.querySelector('.badge');
|
||||
var heading = card.querySelector('h4');
|
||||
var details = card.querySelectorAll('.clients-mobile-client-details strong');
|
||||
var pauseButton = card.querySelector('button[type="submit"].btn-info');
|
||||
var blackoutButton = card.querySelector('button[type="submit"].btn-secondary, button[type="submit"].btn-success');
|
||||
card.setAttribute('data-client-screen-slug', client.screen_slug || '');
|
||||
card.setAttribute('data-client-client-id', client.clientId || client.id || '');
|
||||
card.setAttribute('data-client-player-base-url', client.player_url || '');
|
||||
card.classList.toggle('card-warning', paused);
|
||||
card.classList.toggle('card-primary', !paused);
|
||||
if (badge) {
|
||||
badge.className = 'badge ' + (blackout ? 'text-bg-secondary' : paused ? 'text-bg-warning' : 'text-bg-success');
|
||||
badge.textContent = blackout ? 'Blackout' : paused ? 'Paused' : 'Live';
|
||||
}
|
||||
if (heading) {
|
||||
heading.textContent = client.client_name || 'Unknown';
|
||||
}
|
||||
if (details[0]) {
|
||||
details[0].textContent = client.screen_name || client.screen_slug || 'Unknown';
|
||||
}
|
||||
if (details[1]) {
|
||||
details[1].textContent = client.currentSlideTitle || 'No slide currently showing';
|
||||
}
|
||||
if (pauseButton) {
|
||||
pauseButton.innerHTML = '<i class="bi ' + (paused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (paused ? 'Resume' : 'Pause');
|
||||
}
|
||||
if (blackoutButton) {
|
||||
blackoutButton.innerHTML = '<i class="bi ' + (blackout ? 'bi-eye' : 'bi-eye-slash') + ' me-1" aria-hidden="true"></i>' + (blackout ? 'Restore' : 'Blackout');
|
||||
blackoutButton.className = 'btn btn-sm ' + (blackout ? 'btn-success' : 'btn-secondary') + ' w-100';
|
||||
}
|
||||
card.querySelectorAll('form').forEach(function (form) {
|
||||
var connectionInput = form.querySelector('input[name="connectionId"]');
|
||||
var baseUrlInput = form.querySelector('input[name="playerBaseUrl"]');
|
||||
if (connectionInput) connectionInput.value = client.clientId || client.id || '';
|
||||
if (baseUrlInput) baseUrlInput.value = client.player_url || '';
|
||||
form.action = '/clients/' + encodeURIComponent(client.screen_slug || '') + '/commands';
|
||||
var blackoutInput = form.querySelector('input[name="blackout"]');
|
||||
if (blackoutInput) blackoutInput.value = blackout ? 'false' : 'true';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleDashboardState(state) {
|
||||
if (!state) {
|
||||
return;
|
||||
@@ -1047,6 +1110,7 @@
|
||||
updateStats(state);
|
||||
updateScreenGrid(state);
|
||||
updateClientTable(state);
|
||||
updateMobileClientCards(state);
|
||||
updateKioskLauncherModal(state);
|
||||
updateDashboardQuickActions(state);
|
||||
updateScreenCommandControls();
|
||||
@@ -1142,10 +1206,24 @@
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('article[data-mobile-client-key] .clients-mobile-client-actions').forEach(function (actions) {
|
||||
if (actions.querySelector('button[data-action="move-screen"]')) {
|
||||
return;
|
||||
}
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'btn btn-sm btn-danger';
|
||||
button.setAttribute('data-action', 'move-screen');
|
||||
button.setAttribute('aria-label', 'Change screen');
|
||||
button.setAttribute('title', 'Change screen');
|
||||
button.innerHTML = '<i class="bi bi-display" aria-hidden="true"></i>';
|
||||
actions.insertBefore(button, actions.children[1] || null);
|
||||
});
|
||||
|
||||
if (typeof elements.modal.addEventListener === 'function') {
|
||||
elements.modal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event && event.relatedTarget ? event.relatedTarget : null;
|
||||
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key]') : null;
|
||||
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key], article[data-mobile-client-key]') : null;
|
||||
if (row) {
|
||||
updateClientMoveModalFromRow(row);
|
||||
}
|
||||
@@ -1162,7 +1240,7 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null;
|
||||
var row = moveButton.closest ? moveButton.closest('tr[data-client-key], article[data-mobile-client-key]') : null;
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
}
|
||||
|
||||
var methodSelect = form.querySelector('[data-api-source-auth-method]');
|
||||
var authCard = form.querySelector('[data-api-source-auth-card]');
|
||||
var authDetailsSection = form.querySelector('[data-api-source-auth-details-section]');
|
||||
var panels = Array.prototype.slice.call(form.querySelectorAll('[data-api-source-auth-panel]'));
|
||||
var bearerTokenInput = form.querySelector('[data-api-source-bearer-token-input]');
|
||||
@@ -36,10 +37,20 @@
|
||||
bearerTokenInput.focus();
|
||||
}
|
||||
|
||||
function updatePanels() {
|
||||
function updatePanels(shouldExpandAuth) {
|
||||
var method = String(methodSelect && methodSelect.value || 'none').trim();
|
||||
var hasAuth = method !== 'none';
|
||||
|
||||
if (authCard) {
|
||||
authCard.hidden = !hasAuth;
|
||||
if (shouldExpandAuth && hasAuth && authCard.classList.contains('collapsed-card')) {
|
||||
var collapseButton = authCard.querySelector('[data-lte-toggle="card-collapse"]');
|
||||
if (collapseButton) {
|
||||
collapseButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authDetailsSection) {
|
||||
authDetailsSection.hidden = !hasAuth;
|
||||
}
|
||||
@@ -60,7 +71,9 @@
|
||||
}
|
||||
|
||||
if (methodSelect) {
|
||||
methodSelect.addEventListener('change', updatePanels);
|
||||
methodSelect.addEventListener('change', function () {
|
||||
updatePanels(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (requestMethodSelect) {
|
||||
@@ -72,6 +85,6 @@
|
||||
updateBearerTokenToggle();
|
||||
}
|
||||
|
||||
updatePanels();
|
||||
updatePanels(false);
|
||||
updateRequestBodyVisibility();
|
||||
}());
|
||||
@@ -106,6 +106,9 @@
|
||||
}
|
||||
|
||||
var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
|
||||
if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(expression)) {
|
||||
return placeholderUtils.renderProgressPlaceholder(item, expression);
|
||||
}
|
||||
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
|
||||
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
|
||||
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
|
||||
@@ -164,8 +167,11 @@
|
||||
'<div class="offcanvas-body">' +
|
||||
'<p class="small text-body-secondary">Placeholders read values from the selected API item. Nested fields use dots.</p>' +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderMathTransforms() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderImageTransform() : '') +
|
||||
'<h3 class="fs-6 mt-4 fw-normal">Progress bar</h3>' +
|
||||
'<p class="small">Use <code>{{progress(current,total,success,light,striped,animated,textless)}}</code> to render a configurable bar, or use start and end date/time fields such as <code>{{progress(start_datetime,end_datetime)}}</code> to show elapsed time. The first color controls the bar and the second controls its background. Choose AdminLTE or announcement colors such as <code>orange</code> or <code>midnight</code>, a hex color, <code>striped</code>, <code>animated</code>, or <code>textless</code> in any order. Set the radius with <code>square</code>, <code>pill</code>, or <code>radius(12px)</code>. The bar height follows the API region text size, and all options after the two field paths are optional.</p>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
return '';
|
||||
}
|
||||
var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
|
||||
if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(expression)) {
|
||||
return placeholderUtils.renderProgressPlaceholder(item, expression);
|
||||
}
|
||||
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
|
||||
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
|
||||
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
|
||||
@@ -224,6 +227,7 @@
|
||||
'<h3 class="fs-6 mt-4 fw-normal">Placeholder paths</h3>' +
|
||||
'<p class="small">Nested values use dots. Missing values render as empty text.</p>' +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderMathTransforms() : '') +
|
||||
'<h3 class="fs-6 mt-4 fw-normal">Date formatting</h3>' +
|
||||
'<p class="small">Use <code>format("MMM D, YYYY")</code> with date fields. Use <code>tz()</code> for a short timezone name and <code>tz_long()</code> for the full timezone name.</p>' +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
|
||||
if (placeholderUtils && typeof placeholderUtils.resolvePlaceholderExpression === 'function' && typeof placeholderUtils.formatPlaceholderValue === 'function') {
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(expression)) {
|
||||
return placeholderUtils.renderProgressPlaceholder(context, expression);
|
||||
}
|
||||
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(context, expression)));
|
||||
});
|
||||
}
|
||||
@@ -248,6 +251,7 @@
|
||||
'<h3 class="fs-6 mt-4 fw-normal">Placeholder paths</h3>' +
|
||||
'<p class="small">Use the available field chips. Nested values use dots, and missing values render as empty text.</p>' +
|
||||
window.placeholderInfo.renderTextTransforms() +
|
||||
window.placeholderInfo.renderMathTransforms() +
|
||||
'<h3 class="fs-6 mt-4 fw-normal">Date and time formatting</h3>' +
|
||||
'<p class="small">Use <code>format("MMM D, YYYY h:mm A")</code> with date fields. Use <code>tz()</code> for a short timezone name and <code>tz_long()</code> for the full timezone name.</p>' +
|
||||
window.placeholderInfo.renderDateFormatTokens() +
|
||||
|
||||
@@ -114,6 +114,9 @@
|
||||
if (!value || typeof value !== 'object' || typeof placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof placeholderUtils.formatPlaceholderValue !== 'function') return '';
|
||||
var rawExpression = String(expression).trim();
|
||||
var normalizedExpression = normalizeWeatherExpression(rawExpression);
|
||||
if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(rawExpression)) {
|
||||
return placeholderUtils.renderProgressPlaceholder(value, rawExpression);
|
||||
}
|
||||
var iconMatch = normalizedExpression.match(/^(?:current\.weather_code|daily\.weather_code\.\d+|hourly\.weather_code\.\d+)\.icon(?:\((\d+)(?:\s*,\s*(\d+))?\))?$/);
|
||||
if (iconMatch) {
|
||||
var codeExpression = normalizedExpression.replace(/\.icon(?:\(.*\))?$/, '');
|
||||
@@ -137,6 +140,7 @@
|
||||
'<div class="offcanvas-body"><p class="small text-body-secondary">Placeholders read values from the selected weather snapshot. Nested fields use dots.</p>' +
|
||||
'<h3 class="h6 mt-3">Weather icons</h3><p class="small text-body-secondary">Use the icon property to insert a Bootstrap weather icon. Add width and height in pixels when sizing is needed.</p><ul class="small"><li><code>{{current.icon}}</code></li><li><code>{{current.icon(48,48)}}</code></li><li><code>{{daily.0.icon(32,24)}}</code></li></ul>' +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderMathTransforms() : '') +
|
||||
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -384,9 +384,106 @@
|
||||
setActiveSection(initialSection);
|
||||
}
|
||||
|
||||
function initEmailTemplatePreviews() {
|
||||
var previewUser = document.querySelector('[data-email-preview-user]');
|
||||
var signedInUser = {
|
||||
username: previewUser ? previewUser.getAttribute('data-username') : '',
|
||||
display_name: previewUser ? previewUser.getAttribute('data-display-name') : '',
|
||||
email: previewUser ? previewUser.getAttribute('data-email') : ''
|
||||
};
|
||||
document.querySelectorAll('[data-email-template-editor]').forEach(function (editor) {
|
||||
var subject = editor.querySelector('[data-email-template-subject]');
|
||||
var body = editor.querySelector('[data-email-template-body]');
|
||||
var alignment = editor.querySelector('[data-email-template-alignment]');
|
||||
var buttonText = editor.querySelector('[data-email-template-button-text]');
|
||||
var preview = editor.querySelector('[data-email-template-preview]');
|
||||
if (!subject || !body || !preview) return;
|
||||
|
||||
function render() {
|
||||
preview.innerHTML = '';
|
||||
var shell = document.createElement('div');
|
||||
shell.className = 'email-template-preview__shell';
|
||||
var brand = document.createElement('div');
|
||||
brand.className = 'email-template-preview__brand';
|
||||
brand.textContent = 'Pulse Signage';
|
||||
var message = document.createElement('div');
|
||||
message.className = 'email-template-preview__message';
|
||||
var sampleValues = {
|
||||
username: signedInUser.username || 'username',
|
||||
display_name: signedInUser.display_name || signedInUser.username || 'Your name',
|
||||
email: signedInUser.email || 'your-email@example.com'
|
||||
};
|
||||
var button = document.createElement('div');
|
||||
button.className = 'email-template-preview__button';
|
||||
var buttonAlignment = alignment ? alignment.value : 'center';
|
||||
var buttonWrap = document.createElement('div');
|
||||
buttonWrap.className = 'email-template-preview__button-wrap';
|
||||
buttonWrap.style.textAlign = buttonAlignment;
|
||||
buttonWrap.appendChild(button);
|
||||
var isVerificationTemplate = Boolean(editor.querySelector('[name="verification_subject"]'));
|
||||
var isInvitationTemplate = Boolean(editor.querySelector('[name="invitation_subject"]'));
|
||||
var previewActionPlaceholder = '[[url]]';
|
||||
button.textContent = buttonText && buttonText.value ? buttonText.value : (isVerificationTemplate ? 'Verify email address' : (isInvitationTemplate ? 'Accept invitation' : 'Reset password'));
|
||||
var previewOrigin = window.location.origin || 'https://pulse-signage.example';
|
||||
var previewVerificationUrl = previewOrigin + '/verify-email?token=preview-token';
|
||||
var previewResetUrl = previewOrigin + '/reset-password?token=preview-token';
|
||||
var previewActionUrl = isVerificationTemplate ? previewVerificationUrl : (isInvitationTemplate ? previewOrigin + '/accept-invite?token=preview-token' : previewResetUrl);
|
||||
var hasExplicitButton = body.value.indexOf('[[action_button]]') !== -1;
|
||||
body.value.split(/\r?\n(?:[ \t]*\r?\n)+/).forEach(function (sourceParagraph) {
|
||||
var line = sourceParagraph.replace(/\[\[(username|display_name|email)\]\]/g, function (_match, key) { return sampleValues[key]; }).replace(previewActionPlaceholder, previewActionUrl);
|
||||
var hasButtonToken = sourceParagraph.indexOf('[[action_button]]') !== -1;
|
||||
var hasUrlToken = !hasExplicitButton && sourceParagraph.indexOf(previewActionPlaceholder) !== -1;
|
||||
if (hasButtonToken) {
|
||||
var buttonParts = line.split('[[action_button]]');
|
||||
if (buttonParts[0]) appendPreviewParagraph(buttonParts[0]);
|
||||
message.appendChild(buttonWrap);
|
||||
if (buttonParts[1]) appendPreviewParagraph(buttonParts[1]);
|
||||
return;
|
||||
}
|
||||
if (hasUrlToken) {
|
||||
var legacyParts = sourceParagraph.split(previewActionPlaceholder);
|
||||
if (legacyParts[0]) appendPreviewParagraph(legacyParts[0]);
|
||||
message.appendChild(buttonWrap);
|
||||
appendPreviewParagraph(previewActionUrl + legacyParts[1]);
|
||||
return;
|
||||
}
|
||||
appendPreviewParagraph(line);
|
||||
});
|
||||
function appendPreviewParagraph(line) {
|
||||
var paragraph = document.createElement('p');
|
||||
paragraph.innerHTML = (line || '\u00a0').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\r?\n/g, '<br>').replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1">$1</a>').replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '<strong>$1</strong>').replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '<em>$1</em>').replace(/\[u\]([\s\S]*?)\[\/u\]/gi, '<u>$1</u>');
|
||||
message.appendChild(paragraph);
|
||||
}
|
||||
preview.appendChild(brand);
|
||||
shell.appendChild(message);
|
||||
preview.appendChild(shell);
|
||||
}
|
||||
|
||||
subject.addEventListener('input', render);
|
||||
body.addEventListener('input', render);
|
||||
if (alignment) alignment.addEventListener('change', render);
|
||||
if (buttonText) buttonText.addEventListener('input', render);
|
||||
editor.querySelectorAll('[data-email-format]').forEach(function (formatButton) {
|
||||
formatButton.addEventListener('click', function () {
|
||||
var tag = formatButton.getAttribute('data-email-format');
|
||||
var start = body.selectionStart;
|
||||
var end = body.selectionEnd;
|
||||
var selected = body.value.slice(start, end) || 'text';
|
||||
var markerPattern = new RegExp('^\\[' + tag + '\\]([\\s\\S]*)\\[\\/' + tag + '\\]$');
|
||||
var replacement = markerPattern.test(selected) ? markerPattern.exec(selected)[1] : '[' + tag + ']' + selected + '[/' + tag + ']';
|
||||
body.setRangeText(replacement, start, end, 'select');
|
||||
body.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
body.focus();
|
||||
});
|
||||
});
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initIconSuggestions();
|
||||
initDefaultAnnouncementIconPicker();
|
||||
initSettingsSectionNavigation();
|
||||
initEmailTemplatePreviews();
|
||||
});
|
||||
}());
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
'</dl>';
|
||||
}
|
||||
|
||||
function renderMathTransforms() {
|
||||
return '<h3 class="fs-6 mt-4 fw-normal">Math transforms</h3>' +
|
||||
'<p class="small">Use <code>add(number)</code>, <code>subtract(number)</code>, <code>multiply(number)</code>, or <code>divide(number)</code>, for example <code>{{amount.multiply(10)}}</code>. Transforms can be chained, such as <code>{{amount.add(3).multiply(10)}}</code>.</p>';
|
||||
}
|
||||
|
||||
function renderDateFormatTokens() {
|
||||
return '<h3 class="fs-6 mt-4 fw-normal">Date format tokens</h3>' +
|
||||
'<p class="small">Use the <code>format("...")</code> transform with these tokens. Text inside square brackets is treated as a literal.</p>' +
|
||||
@@ -61,6 +66,7 @@
|
||||
escapeHtml: escapeHtml,
|
||||
render: render,
|
||||
renderTextTransforms: renderTextTransforms,
|
||||
renderMathTransforms: renderMathTransforms,
|
||||
renderDateFormatTokens: renderDateFormatTokens,
|
||||
renderImageTransform: renderImageTransform
|
||||
};
|
||||
|
||||
@@ -27,9 +27,30 @@
|
||||
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 ? raw.split('.') : [];
|
||||
var segments = raw ? splitExpressionSegments(raw) : [];
|
||||
var transforms = [];
|
||||
|
||||
while (segments.length) {
|
||||
@@ -57,13 +78,33 @@
|
||||
if (!source) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((source[0] === '"' && source[source.length - 1] === '"') || (source[0] === '\'' && source[source.length - 1] === '\'')) {
|
||||
return [source.slice(1, -1)];
|
||||
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 source.split(',').map(function (item) {
|
||||
return String(item || '').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);
|
||||
}
|
||||
|
||||
@@ -253,6 +294,26 @@
|
||||
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();
|
||||
@@ -284,6 +345,18 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -319,6 +392,147 @@
|
||||
};
|
||||
}
|
||||
|
||||
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 '';
|
||||
@@ -372,6 +586,8 @@
|
||||
resolvePlaceholderExpression: resolvePlaceholderExpression,
|
||||
isImagePlaceholderExpression: isImagePlaceholderExpression,
|
||||
getImagePlaceholderConfig: getImagePlaceholderConfig,
|
||||
isProgressPlaceholderExpression: isProgressPlaceholderExpression,
|
||||
renderProgressPlaceholder: renderProgressPlaceholder,
|
||||
formatPlaceholderValue: formatPlaceholderValue,
|
||||
collectPlaceholderFieldPaths: collectPlaceholderFieldPaths
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
? placeholderChips.renderChip(field)
|
||||
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('');
|
||||
var transformHint = includeTransformHint === false ? '' : '<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code>, <code>{{publishedAt.format("MMM D, YYYY")}}</code>.</div>';
|
||||
var transformHint = includeTransformHint === false ? '' : '<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code>, <code>{{publishedAt.format("MMM D, YYYY")}}</code>, or <code>{{amount.multiply(10)}}</code>.</div>';
|
||||
|
||||
if (!overflowFields.length) {
|
||||
return visibleMarkup + transformHint;
|
||||
|
||||
@@ -129,6 +129,26 @@
|
||||
|
||||
input.value = String(currentUrl.searchParams.get(searchParam) || '').trim();
|
||||
|
||||
var searchGroup = input.closest('.table-search-group');
|
||||
if (searchGroup) {
|
||||
var actionButton = searchGroup.parentNode ? searchGroup.parentNode.querySelector('.btn:not(.table-search-group .btn)') : null;
|
||||
if (actionButton && actionButton.getBoundingClientRect) {
|
||||
searchGroup.style.setProperty('--table-search-action-offset', (actionButton.getBoundingClientRect().width + 8) + 'px');
|
||||
}
|
||||
var searchToggle = searchGroup.querySelector('[data-table-search-toggle]');
|
||||
if (searchToggle) {
|
||||
searchToggle.addEventListener('click', function () {
|
||||
focusSearchInput(input);
|
||||
});
|
||||
searchToggle.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
focusSearchInput(input);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearch() {
|
||||
var query = String(input.value || '').trim();
|
||||
var nextUrl = new URL(window.location.href);
|
||||
|
||||
Reference in New Issue
Block a user