Release 2.8.6
This commit is contained in:
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## 2.8.5 - 2026-08-17
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.8.5",
|
"version": "2.8.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.8.5",
|
"version": "2.8.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.8.5",
|
"version": "2.8.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.8.5",
|
"version": "2.8.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sparticuz/chromium": "^149.0.0",
|
"@sparticuz/chromium": "^149.0.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.8.5",
|
"version": "2.8.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -886,7 +886,7 @@
|
|||||||
var summaryLabel = isRoot ? (isArray ? 'Array' : 'Object') : String(key);
|
var summaryLabel = isRoot ? (isArray ? 'Array' : 'Object') : String(key);
|
||||||
var summaryMeta = isArray ? '[' + value.length + ']' : '{' + Object.keys(value).length + '}';
|
var summaryMeta = isArray ? '[' + value.length + ']' : '{' + Object.keys(value).length + '}';
|
||||||
return '' +
|
return '' +
|
||||||
'<details class="json-tree-node" open>' +
|
'<details class="json-tree-node"' + (isRoot ? ' open' : '') + '>' +
|
||||||
'<summary><span class="json-tree-key">' + escapeHtml(summaryLabel) + '</span><span class="mx-1">:</span><span class="text-body-secondary">' + escapeHtml(summaryMeta) + '</span></summary>' +
|
'<summary><span class="json-tree-key">' + escapeHtml(summaryLabel) + '</span><span class="mx-1">:</span><span class="text-body-secondary">' + escapeHtml(summaryMeta) + '</span></summary>' +
|
||||||
'<div class="ms-3 ps-3 border-start">' + entries + '</div>' +
|
'<div class="ms-3 ps-3 border-start">' + entries + '</div>' +
|
||||||
'</details>';
|
'</details>';
|
||||||
@@ -945,26 +945,30 @@
|
|||||||
detail.open = expanded;
|
detail.open = expanded;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (collapseButton) {
|
syncButtons();
|
||||||
collapseButton.disabled = !details.length || !expanded;
|
|
||||||
}
|
|
||||||
if (expandButton) {
|
|
||||||
expandButton.disabled = !details.length || expanded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function 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) {
|
if (collapseButton) {
|
||||||
collapseButton.setAttribute('aria-pressed', 'false');
|
collapseButton.disabled = !details.length || allCollapsed;
|
||||||
|
collapseButton.setAttribute('aria-pressed', String(allCollapsed));
|
||||||
}
|
}
|
||||||
if (expandButton) {
|
if (expandButton) {
|
||||||
expandButton.setAttribute('aria-pressed', 'true');
|
expandButton.disabled = !details.length || allExpanded;
|
||||||
|
expandButton.setAttribute('aria-pressed', String(allExpanded));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.innerHTML = formattedTree;
|
output.innerHTML = formattedTree;
|
||||||
|
setAllSectionsExpanded(false);
|
||||||
|
var rootDetails = output.querySelector('details');
|
||||||
|
if (rootDetails) {
|
||||||
|
rootDetails.open = true;
|
||||||
|
}
|
||||||
syncButtons();
|
syncButtons();
|
||||||
setAllSectionsExpanded(true);
|
|
||||||
|
|
||||||
if (collapseButton) {
|
if (collapseButton) {
|
||||||
collapseButton.addEventListener('click', function () {
|
collapseButton.addEventListener('click', function () {
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="card-body p-3 d-grid gap-2">' +
|
'<div class="card-body p-3 d-grid gap-2">' +
|
||||||
'<label class="form-label mb-1" for="region_qr_code_' + region.id + '">URL</label>' +
|
'<label class="form-label mb-1" for="region_qr_code_' + region.id + '">URL</label>' +
|
||||||
'<input id="region_qr_code_' + region.id + '" type="url" name="region_qr_code_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
'<input id="region_qr_code_' + region.id + '" type="url" name="region_qr_code_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" required />' +
|
||||||
'<textarea name="region_qr_svg_' + region.id + '" class="visually-hidden" hidden>' + escapeHtml(currentSvg) + '</textarea>' +
|
'<textarea name="region_qr_svg_' + region.id + '" class="visually-hidden" hidden>' + escapeHtml(currentSvg) + '</textarea>' +
|
||||||
'<input type="hidden" name="region_qr_preview_' + region.id + '" value="' + escapeHtml(String(context.qr_preview || '')) + '" />' +
|
'<input type="hidden" name="region_qr_preview_' + region.id + '" value="' + escapeHtml(String(context.qr_preview || '')) + '" />' +
|
||||||
'<div class="card card-outline card-secondary overflow-hidden mt-2">' +
|
'<div class="card card-outline card-secondary overflow-hidden mt-2">' +
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
headerActions: '<span class="chip">Webpage</span>',
|
headerActions: '<span class="chip">Webpage</span>',
|
||||||
bodyHtml: '' +
|
bodyHtml: '' +
|
||||||
'<div class="input-group flex-nowrap">' +
|
'<div class="input-group flex-nowrap">' +
|
||||||
'<input type="url" name="region_webpage_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
'<input type="url" name="region_webpage_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" required />' +
|
||||||
'<button type="button" class="btn btn-outline-secondary text-nowrap" data-webpage-preview-update data-region-id="' + region.id + '">Update</button>' +
|
'<button type="button" class="btn btn-outline-secondary text-nowrap" data-webpage-preview-update data-region-id="' + region.id + '">Update</button>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -842,6 +842,15 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
|||||||
requestPreviewRender();
|
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) {
|
slideForm.addEventListener('submit', function (event) {
|
||||||
if (!window.webAsyncSaveForm || typeof window.webAsyncSaveForm.submit !== 'function') {
|
if (!window.webAsyncSaveForm || typeof window.webAsyncSaveForm.submit !== 'function') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,98 @@
|
|||||||
// Shared browser helpers for web UI escaping and formatting.
|
// Shared browser helpers for web UI escaping and formatting.
|
||||||
|
|
||||||
(function () {
|
(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) {
|
function escapeHtml(value) {
|
||||||
return String(value ?? '')
|
return String(value ?? '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -97,6 +189,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.escapeHtml = escapeHtml;
|
window.escapeHtml = escapeHtml;
|
||||||
|
initializeUrlValidation();
|
||||||
|
|
||||||
var defaultQrOptions = window.defaultQrOptions || {
|
var defaultQrOptions = window.defaultQrOptions || {
|
||||||
width: 1000,
|
width: 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user