- manifest.webmanifest + service worker (offline shell, /api network-only, asset stale-while-revalidate) - set icone (192/512 + maskable + apple-touch + favicon), generate zero-deps (pure Node+zlib) - tag PWA + viewport viewport-fit=cover (zoom mantenuto, WCAG 1.4.4) in 77 pagine HTML - AGID/WCAG 2.1 AA: touch target >=44px, orientamento/zoom liberi, safe-area standalone - generatori riproducibili: scripts/gen-pwa-icons.mjs + scripts/inject-pwa-head.mjs - bump version 1.14.1 -> 1.15.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
5.1 KiB
JavaScript
131 lines
5.1 KiB
JavaScript
/*
|
|
* NIS2 Agile — PWA bootstrap
|
|
* Registra il service worker e gestisce l'UI di installazione (Android + iOS).
|
|
* Vanilla JS, nessuna dipendenza. Localizzato IT/EN (chiave i18n: 'nis2_lang').
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
// --- i18n minimale, allineato a i18n.js (localStorage 'nis2_lang') ---
|
|
function lang() {
|
|
try { return (localStorage.getItem('nis2_lang') || 'it').slice(0, 2); }
|
|
catch (e) { return 'it'; }
|
|
}
|
|
var STR = {
|
|
it: {
|
|
install: 'Installa app',
|
|
banner: 'Installa NIS2 Agile sul dispositivo per un accesso più rapido.',
|
|
ios: 'Per installare: tocca Condividi e poi "Aggiungi a Home".',
|
|
close: 'Chiudi'
|
|
},
|
|
en: {
|
|
install: 'Install app',
|
|
banner: 'Install NIS2 Agile on your device for faster access.',
|
|
ios: 'To install: tap Share, then "Add to Home Screen".',
|
|
close: 'Close'
|
|
}
|
|
};
|
|
function t(k) { var l = lang(); return (STR[l] && STR[l][k]) || STR.it[k]; }
|
|
|
|
function isStandalone() {
|
|
return (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|
|
|| window.navigator.standalone === true;
|
|
}
|
|
|
|
// --- 1. Registrazione service worker ---
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', function () {
|
|
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
|
.catch(function (err) { console.warn('[pwa] SW registration failed', err); });
|
|
});
|
|
}
|
|
|
|
// Gia' installata (standalone): niente banner di installazione.
|
|
if (isStandalone()) return;
|
|
|
|
// --- UI banner sobrio, indipendente dal CSS dell'app ---
|
|
function makeBanner(message, withInstallBtn) {
|
|
var bar = document.createElement('div');
|
|
bar.setAttribute('role', 'region');
|
|
bar.setAttribute('aria-label', t('install'));
|
|
bar.style.cssText = 'position:fixed;left:12px;right:12px;bottom:12px;z-index:9999;'
|
|
+ 'background:#0066CC;color:#fff;border-radius:10px;padding:12px 14px;'
|
|
+ 'box-shadow:0 6px 24px rgba(0,0,0,.25);display:flex;align-items:center;gap:10px;'
|
|
+ 'font-family:system-ui,Arial,sans-serif;font-size:.95rem;max-width:560px;margin:0 auto;';
|
|
|
|
var txt = document.createElement('span');
|
|
txt.style.cssText = 'flex:1;line-height:1.35;';
|
|
txt.textContent = message;
|
|
bar.appendChild(txt);
|
|
|
|
var btnInstall = null;
|
|
if (withInstallBtn) {
|
|
btnInstall = document.createElement('button');
|
|
btnInstall.type = 'button';
|
|
btnInstall.textContent = t('install');
|
|
btnInstall.style.cssText = 'background:#fff;color:#0066CC;border:0;border-radius:8px;'
|
|
+ 'padding:10px 16px;font-weight:600;min-height:44px;cursor:pointer;white-space:nowrap;';
|
|
bar.appendChild(btnInstall);
|
|
}
|
|
|
|
var btnClose = document.createElement('button');
|
|
btnClose.type = 'button';
|
|
btnClose.setAttribute('aria-label', t('close'));
|
|
btnClose.textContent = '✕';
|
|
btnClose.style.cssText = 'background:transparent;color:#fff;border:0;font-size:1.1rem;'
|
|
+ 'line-height:1;min-width:44px;min-height:44px;cursor:pointer;';
|
|
bar.appendChild(btnClose);
|
|
|
|
document.body.appendChild(bar);
|
|
return { bar: bar, btnInstall: btnInstall, btnClose: btnClose };
|
|
}
|
|
|
|
// --- 2. Android / Chrome: beforeinstallprompt ---
|
|
var deferred = null;
|
|
var DISMISS_KEY = 'pwa-install-dismissed';
|
|
function dismissed() { try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch (e) { return false; } }
|
|
function setDismissed() { try { localStorage.setItem(DISMISS_KEY, '1'); } catch (e) {} }
|
|
|
|
window.addEventListener('beforeinstallprompt', function (e) {
|
|
e.preventDefault();
|
|
deferred = e;
|
|
if (dismissed()) return;
|
|
try {
|
|
var ui = makeBanner(t('banner'), true);
|
|
ui.btnInstall.addEventListener('click', function () {
|
|
ui.bar.remove();
|
|
if (!deferred) return;
|
|
deferred.prompt();
|
|
if (deferred.userChoice && deferred.userChoice.finally) {
|
|
deferred.userChoice.finally(function () { deferred = null; });
|
|
} else { deferred = null; }
|
|
});
|
|
ui.btnClose.addEventListener('click', function () { ui.bar.remove(); setDismissed(); });
|
|
} catch (err) { console.warn('[pwa] banner error', err); }
|
|
});
|
|
|
|
window.addEventListener('appinstalled', function () {
|
|
deferred = null;
|
|
setDismissed();
|
|
});
|
|
|
|
// --- 3. iOS Safari: nessun beforeinstallprompt -> hint manuale (una volta) ---
|
|
try {
|
|
var ua = navigator.userAgent || '';
|
|
var isIOS = /iphone|ipad|ipod/i.test(ua)
|
|
|| (/Macintosh/.test(ua) && 'ontouchend' in document); // iPad iPadOS desktop-mode
|
|
var isSafari = /safari/i.test(ua) && !/crios|fxios|edgios|android/i.test(ua);
|
|
var IOS_KEY = 'pwa-ios-hint-dismissed';
|
|
var iosDismissed = (function () { try { return localStorage.getItem(IOS_KEY) === '1'; } catch (e) { return false; } })();
|
|
if (isIOS && isSafari && !iosDismissed) {
|
|
window.addEventListener('load', function () {
|
|
var ui = makeBanner(t('ios'), false);
|
|
ui.btnClose.addEventListener('click', function () {
|
|
ui.bar.remove();
|
|
try { localStorage.setItem(IOS_KEY, '1'); } catch (e) {}
|
|
});
|
|
});
|
|
}
|
|
} catch (err) { console.warn('[pwa] ios hint error', err); }
|
|
})();
|