diff --git a/CHANGELOG.md b/CHANGELOG.md index 04a71c0..f67bb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## 2.8.6 - 2026-08-18 + +### Fixed + +- Added live URL validation with inline feedback and explicit HTTP or HTTPS scheme enforcement for URL fields. +- Prevented Enter in slide editor inputs from implicitly saving the slide. +- Collapsed nested API response JSON sections by default while keeping the root response visible. + ## 2.8.5 - 2026-08-17 ### Fixed diff --git a/build/package.player.json b/build/package.player.json index 92f94ea..b61fcd6 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.8.5", + "version": "2.8.6", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 606b9a5..6f71dbe 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.8.5", + "version": "2.8.6", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 798fb10..1c4a690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.8.5", + "version": "2.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.8.5", + "version": "2.8.6", "dependencies": { "@sparticuz/chromium": "^149.0.0", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index 400d503..f187e1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.8.5", + "version": "2.8.6", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/public/js/admin/admin-page.js b/src/web/public/js/admin/admin-page.js index 27a7c13..7d9371c 100644 --- a/src/web/public/js/admin/admin-page.js +++ b/src/web/public/js/admin/admin-page.js @@ -886,7 +886,7 @@ var summaryLabel = isRoot ? (isArray ? 'Array' : 'Object') : String(key); var summaryMeta = isArray ? '[' + value.length + ']' : '{' + Object.keys(value).length + '}'; return '' + - '
' + + '
' + '' + escapeHtml(summaryLabel) + ':' + escapeHtml(summaryMeta) + '' + '
' + entries + '
' + '
'; @@ -945,26 +945,30 @@ detail.open = expanded; }); - if (collapseButton) { - collapseButton.disabled = !details.length || !expanded; - } - if (expandButton) { - expandButton.disabled = !details.length || expanded; - } + syncButtons(); } function syncButtons() { + var details = output.querySelectorAll('details'); + var allExpanded = details.length > 0 && Array.prototype.every.call(details, function (detail) { return detail.open; }); + var allCollapsed = details.length > 0 && Array.prototype.every.call(details, function (detail) { return !detail.open; }); if (collapseButton) { - collapseButton.setAttribute('aria-pressed', 'false'); + collapseButton.disabled = !details.length || allCollapsed; + collapseButton.setAttribute('aria-pressed', String(allCollapsed)); } if (expandButton) { - expandButton.setAttribute('aria-pressed', 'true'); + expandButton.disabled = !details.length || allExpanded; + expandButton.setAttribute('aria-pressed', String(allExpanded)); } } output.innerHTML = formattedTree; + setAllSectionsExpanded(false); + var rootDetails = output.querySelector('details'); + if (rootDetails) { + rootDetails.open = true; + } syncButtons(); - setAllSectionsExpanded(true); if (collapseButton) { collapseButton.addEventListener('click', function () { diff --git a/src/web/public/js/regions/type/qr-code.js b/src/web/public/js/regions/type/qr-code.js index 626c283..f3240fa 100644 --- a/src/web/public/js/regions/type/qr-code.js +++ b/src/web/public/js/regions/type/qr-code.js @@ -1029,7 +1029,7 @@ '' + '
' + '' + - '' + + '' + '' + '' + '
' + diff --git a/src/web/public/js/regions/type/webpage.js b/src/web/public/js/regions/type/webpage.js index 9bbd1e9..4222226 100644 --- a/src/web/public/js/regions/type/webpage.js +++ b/src/web/public/js/regions/type/webpage.js @@ -50,7 +50,7 @@ headerActions: 'Webpage', bodyHtml: '' + '
' + - '' + + '' + '' + '
' }); diff --git a/src/web/public/js/slides/slide-form.js b/src/web/public/js/slides/slide-form.js index cf49ffa..79e5759 100644 --- a/src/web/public/js/slides/slide-form.js +++ b/src/web/public/js/slides/slide-form.js @@ -842,6 +842,15 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev requestPreviewRender(); } + slideForm.addEventListener('keydown', function (event) { + var target = event.target; + if (event.key !== 'Enter' || !target || !target.matches || !target.matches('input:not([type="submit"]):not([type="button"]):not([type="reset"]), select')) { + return; + } + + event.preventDefault(); + }); + slideForm.addEventListener('submit', function (event) { if (!window.webAsyncSaveForm || typeof window.webAsyncSaveForm.submit !== 'function') { return; diff --git a/src/web/public/js/web-ui-helpers.js b/src/web/public/js/web-ui-helpers.js index bda1878..d371238 100644 --- a/src/web/public/js/web-ui-helpers.js +++ b/src/web/public/js/web-ui-helpers.js @@ -1,6 +1,98 @@ // Shared browser helpers for web UI escaping and formatting. (function () { + var urlValidationMessage = 'Please enter a URL.'; + + function isUrlInput(input) { + return input && input.matches && input.matches('input[type="url"]'); + } + + function isValidHttpUrl(input) { + input.setCustomValidity(''); + var value = String(input.value || '').trim(); + var hasValidNativeUrl = input.validity.valid; + var hasExplicitHttpScheme = /^https?:\/\/[^\s]+$/i.test(value); + var isValid = hasValidNativeUrl && hasExplicitHttpScheme; + if (!isValid) { + input.setCustomValidity(urlValidationMessage); + } + return isValid; + } + + function getUrlFeedback(input) { + if (input._urlFeedback && input._urlFeedback.parentNode) { + return input._urlFeedback; + } + + var feedbackId = input.id ? input.id + '-url-feedback' : ''; + var feedback = feedbackId ? document.getElementById(feedbackId) : null; + if (feedback) { + var inputGroup = input.closest ? input.closest('.input-group') : null; + var feedbackAnchor = inputGroup || input; + feedbackAnchor.parentNode.insertBefore(feedback, feedbackAnchor.nextSibling); + input._urlFeedback = feedback; + return feedback; + } + + feedback = document.createElement('div'); + if (feedbackId) { + feedback.id = feedbackId; + } + feedback.className = 'invalid-feedback'; + feedback.setAttribute('role', 'alert'); + feedback.textContent = urlValidationMessage; + + var inputGroup = input.closest ? input.closest('.input-group') : null; + var feedbackAnchor = inputGroup || input; + feedbackAnchor.parentNode.insertBefore(feedback, feedbackAnchor.nextSibling); + if (input.id) { + input.setAttribute('aria-describedby', feedback.id); + } + input._urlFeedback = feedback; + return feedback; + } + + function updateUrlValidation(input, showFeedback) { + if (!isUrlInput(input)) { + return true; + } + + var isValid = isValidHttpUrl(input); + if (!isValid && showFeedback) { + var feedback = getUrlFeedback(input); + feedback.hidden = false; + feedback.classList.add('d-block'); + input.classList.add('is-invalid'); + } else if (isValid) { + if (input.id) { + document.querySelectorAll('[id="' + input.id + '-url-feedback"]').forEach(function (feedback) { + feedback.hidden = true; + feedback.classList.remove('d-block'); + }); + } else if (input._urlFeedback) { + input._urlFeedback.hidden = true; + input._urlFeedback.classList.remove('d-block'); + } + input.classList.remove('is-invalid'); + } + + return isValid; + } + + function initializeUrlValidation() { + document.addEventListener('input', function (event) { + if (isUrlInput(event.target)) { + updateUrlValidation(event.target, true); + } + }); + document.addEventListener('invalid', function (event) { + if (isUrlInput(event.target)) { + event.preventDefault(); + updateUrlValidation(event.target, true); + } + }, true); + } + function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') @@ -97,6 +189,7 @@ } window.escapeHtml = escapeHtml; + initializeUrlValidation(); var defaultQrOptions = window.defaultQrOptions || { width: 1000,