@modernman00/shared-js-lib 1.2.81 → 1.2.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AcctMgt/AdminAuthForm.js +26 -0
- package/AcctMgt/createFormHandler.js +25 -13
- package/AcctMgt/processAll.js +23 -1
- package/AcctMgt/recaptchaToken.js +25 -10
- package/AcctMgt/update.js +66 -33
- package/Alert.js +158 -0
- package/AuthInterceptor.js +56 -0
- package/ButtonLoader.js +133 -0
- package/CardCanvasGenerator.js +367 -0
- package/Clipboard.js +45 -0
- package/Cookie.js +1 -19
- package/FormLock.js +58 -0
- package/Http.js +158 -51
- package/InlineValidation.js +78 -0
- package/NetworkGuard.js +54 -0
- package/OfflineQueue.js +278 -0
- package/PwaInstall.js +165 -0
- package/PwaManager.js +224 -0
- package/README.md +74 -0
- package/ShareManager.js +462 -0
- package/ShowResponse.js +120 -0
- package/axiosWrapper.js +3 -0
- package/config/version.php +1 -1
- package/index.js +14 -1
- package/package.json +18 -3
- package/release-history.md +35 -0
- package/templates/sw.template.js +239 -0
package/PwaInstall.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📱 PwaInstall — Smart A2HS Banner & iOS Safari Installation Engine
|
|
3
|
+
* Part of @modernman00/shared-js-lib
|
|
4
|
+
*
|
|
5
|
+
* Implements:
|
|
6
|
+
* 1. Native beforeinstallprompt interception for Chromium
|
|
7
|
+
* 2. iOS Safari step-by-step walkthrough bottom-sheet
|
|
8
|
+
* 3. Configurable app name, icon, and brand colors
|
|
9
|
+
* 4. 14-day persistent dismissal window
|
|
10
|
+
* 5. Safe-area notch padding & 44px minimum tap target compliance
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class PwaInstall {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.appName = options.appName || 'Application';
|
|
16
|
+
this.appIcon = options.appIcon || '/public/img/favicon/apple-touch-icon.png';
|
|
17
|
+
this.brandColor = options.brandColor || '#2563eb';
|
|
18
|
+
this.dismissKey = options.dismissKey || `${this.appName.toLowerCase().replace(/[^a-z0-9]/g, '_')}_pwa_dismissed`;
|
|
19
|
+
this.dismissDurationMs = (options.dismissDays || 14) * 24 * 60 * 60 * 1000;
|
|
20
|
+
this.delayMs = options.delayMs || 3000;
|
|
21
|
+
this.deferredPrompt = null;
|
|
22
|
+
this.isInitialized = false;
|
|
23
|
+
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
this.init();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
init() {
|
|
30
|
+
if (this.isInitialized || typeof window === 'undefined') return;
|
|
31
|
+
this.isInitialized = true;
|
|
32
|
+
|
|
33
|
+
// Check if running in standalone mode (already installed)
|
|
34
|
+
const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|
|
35
|
+
|| window.navigator.standalone === true;
|
|
36
|
+
|
|
37
|
+
if (isStandalone) return;
|
|
38
|
+
|
|
39
|
+
// Check if dismissed recently
|
|
40
|
+
const dismissedAt = localStorage.getItem(this.dismissKey);
|
|
41
|
+
if (dismissedAt && (Date.now() - parseInt(dismissedAt, 10) < this.dismissDurationMs)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
|
46
|
+
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
47
|
+
|
|
48
|
+
// 1. Chromium native beforeinstallprompt
|
|
49
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
this.deferredPrompt = e;
|
|
52
|
+
this.showBanner('native');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 2. iOS Safari instructions
|
|
56
|
+
if (isIOS && isSafari && !isStandalone) {
|
|
57
|
+
if (document.readyState === 'complete') {
|
|
58
|
+
setTimeout(() => this.showBanner('ios'), this.delayMs);
|
|
59
|
+
} else {
|
|
60
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
61
|
+
setTimeout(() => this.showBanner('ios'), this.delayMs);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
showBanner(type) {
|
|
68
|
+
if (document.getElementById('pwaUniversalInstallBanner')) return;
|
|
69
|
+
|
|
70
|
+
const banner = document.createElement('div');
|
|
71
|
+
banner.id = 'pwaUniversalInstallBanner';
|
|
72
|
+
banner.setAttribute('role', 'dialog');
|
|
73
|
+
banner.setAttribute('aria-label', `Install ${this.appName}`);
|
|
74
|
+
|
|
75
|
+
// Styling with glassmorphism and safe-area compliance
|
|
76
|
+
banner.style.position = 'fixed';
|
|
77
|
+
banner.style.bottom = 'max(16px, env(safe-area-inset-bottom, 16px))';
|
|
78
|
+
banner.style.left = '16px';
|
|
79
|
+
banner.style.right = '16px';
|
|
80
|
+
banner.style.margin = '0 auto';
|
|
81
|
+
banner.style.maxWidth = '480px';
|
|
82
|
+
banner.style.backgroundColor = 'rgba(23, 23, 28, 0.94)';
|
|
83
|
+
banner.style.backdropFilter = 'blur(12px)';
|
|
84
|
+
banner.style.webkitBackdropFilter = 'blur(12px)';
|
|
85
|
+
banner.style.border = '1px solid rgba(255, 255, 255, 0.12)';
|
|
86
|
+
banner.style.borderRadius = '16px';
|
|
87
|
+
banner.style.boxShadow = '0 12px 36px rgba(0, 0, 0, 0.45)';
|
|
88
|
+
banner.style.padding = '14px 16px';
|
|
89
|
+
banner.style.display = 'flex';
|
|
90
|
+
banner.style.alignItems = 'center';
|
|
91
|
+
banner.style.justifyContent = 'space-between';
|
|
92
|
+
banner.style.gap = '12px';
|
|
93
|
+
banner.style.zIndex = '99999';
|
|
94
|
+
banner.style.color = '#ffffff';
|
|
95
|
+
banner.style.fontFamily = 'system-ui, -apple-system, sans-serif';
|
|
96
|
+
banner.style.transition = 'all 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
|
|
97
|
+
|
|
98
|
+
const iconHtml = `<img src="${this.appIcon}" alt="${this.appName} Logo" style="width: 44px; height: 44px; border-radius: 10px; object-fit: cover; flex-shrink: 0;">`;
|
|
99
|
+
|
|
100
|
+
if (type === 'native') {
|
|
101
|
+
banner.innerHTML = `
|
|
102
|
+
<div style="display: flex; align-items: center; gap: 12px; min-width: 0;">
|
|
103
|
+
${iconHtml}
|
|
104
|
+
<div style="min-width: 0;">
|
|
105
|
+
<strong style="display: block; font-size: 15px; font-weight: 600; line-height: 1.2;">Install ${this.appName}</strong>
|
|
106
|
+
<span style="display: block; font-size: 12px; color: #a1a1aa; margin-top: 2px;">Add to home screen for fast, offline access</span>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0;">
|
|
110
|
+
<button id="btnPwaInstallAction" type="button" style="background-color: ${this.brandColor}; color: #ffffff; border: none; border-radius: 8px; padding: 8px 14px; font-size: 13px; font-weight: 600; cursor: pointer; min-height: 44px; min-width: 44px;">
|
|
111
|
+
Install
|
|
112
|
+
</button>
|
|
113
|
+
<button id="btnPwaDismissAction" type="button" aria-label="Dismiss" style="background: none; border: none; color: #a1a1aa; font-size: 20px; cursor: pointer; padding: 8px; min-height: 44px; min-width: 44px;">
|
|
114
|
+
×
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
`;
|
|
118
|
+
} else {
|
|
119
|
+
banner.innerHTML = `
|
|
120
|
+
<div style="display: flex; align-items: center; gap: 12px; min-width: 0;">
|
|
121
|
+
${iconHtml}
|
|
122
|
+
<div style="min-width: 0;">
|
|
123
|
+
<strong style="display: block; font-size: 15px; font-weight: 600; line-height: 1.2;">Install on iPhone</strong>
|
|
124
|
+
<span style="display: block; font-size: 12px; color: #a1a1aa; margin-top: 2px;">Tap <strong>Share</strong>, then <strong>'Add to Home Screen'</strong></span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0;">
|
|
128
|
+
<button id="btnPwaDismissAction" type="button" aria-label="Dismiss" style="background: none; border: none; color: #a1a1aa; font-size: 20px; cursor: pointer; padding: 8px; min-height: 44px; min-width: 44px;">
|
|
129
|
+
×
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
document.body.appendChild(banner);
|
|
136
|
+
|
|
137
|
+
const installBtn = document.getElementById('btnPwaInstallAction');
|
|
138
|
+
if (installBtn && this.deferredPrompt) {
|
|
139
|
+
installBtn.addEventListener('click', async () => {
|
|
140
|
+
banner.style.display = 'none';
|
|
141
|
+
this.deferredPrompt.prompt();
|
|
142
|
+
const { outcome } = await this.deferredPrompt.userChoice;
|
|
143
|
+
if (outcome === 'accepted') {
|
|
144
|
+
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
|
145
|
+
window.dispatchEvent(new CustomEvent('pwa:installed', { detail: { appName: this.appName } }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.deferredPrompt = null;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const dismissBtn = document.getElementById('btnPwaDismissAction');
|
|
153
|
+
if (dismissBtn) {
|
|
154
|
+
dismissBtn.addEventListener('click', () => {
|
|
155
|
+
banner.style.opacity = '0';
|
|
156
|
+
banner.style.transform = 'translateY(16px)';
|
|
157
|
+
setTimeout(() => banner.remove(), 300);
|
|
158
|
+
localStorage.setItem(this.dismissKey, Date.now().toString());
|
|
159
|
+
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
|
160
|
+
window.dispatchEvent(new CustomEvent('pwa:install-dismissed', { detail: { appName: this.appName } }));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
package/PwaManager.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📱 PwaManager — Universal Cross-App Service Worker & PWA Controller
|
|
3
|
+
* Part of @modernman00/shared-js-lib
|
|
4
|
+
*
|
|
5
|
+
* Implements:
|
|
6
|
+
* 1. Clean root /sw.js registration with scope '/'
|
|
7
|
+
* 2. Purging of legacy orphaned workers (/public/sw.js, /service-worker.js)
|
|
8
|
+
* 3. User-prompted updates via 'postMessage' with zero jarring auto-reloads
|
|
9
|
+
* 4. Safe cache purging on user logout (clears API & session caches)
|
|
10
|
+
* 5. Custom event dispatching for seamless Blade UI integration
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class PwaManager {
|
|
14
|
+
static registration = null;
|
|
15
|
+
static options = {
|
|
16
|
+
swUrl: '/sw.js',
|
|
17
|
+
scope: '/',
|
|
18
|
+
appName: 'App',
|
|
19
|
+
enableUpdateToast: true,
|
|
20
|
+
enableBadgingSync: true,
|
|
21
|
+
badgeEndpoint: '/api/notifications/unread',
|
|
22
|
+
onUpdateAvailable: null
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes Service Worker registration and lifecycle monitoring
|
|
27
|
+
*/
|
|
28
|
+
static async init(userOptions = {}) {
|
|
29
|
+
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.options = { ...this.options, ...userOptions };
|
|
34
|
+
|
|
35
|
+
window.addEventListener('load', async () => {
|
|
36
|
+
try {
|
|
37
|
+
await this.purgeLegacyWorkers();
|
|
38
|
+
this.registration = await navigator.serviceWorker.register(this.options.swUrl, {
|
|
39
|
+
scope: this.options.scope
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(`[PwaManager] ${this.options.appName} ServiceWorker active with scope:`, this.registration.scope);
|
|
43
|
+
|
|
44
|
+
// Listen for new workers entering 'installing' or 'waiting' states
|
|
45
|
+
this.registration.addEventListener('updatefound', () => {
|
|
46
|
+
const installingWorker = this.registration.installing;
|
|
47
|
+
if (!installingWorker) return;
|
|
48
|
+
|
|
49
|
+
installingWorker.addEventListener('statechange', () => {
|
|
50
|
+
if (installingWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
51
|
+
console.log(`[PwaManager] New version of ${this.options.appName} installed and waiting.`);
|
|
52
|
+
this.handleUpdateAvailable(installingWorker);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Detect if a worker was already waiting prior to page load
|
|
58
|
+
if (this.registration.waiting && navigator.serviceWorker.controller) {
|
|
59
|
+
this.handleUpdateAvailable(this.registration.waiting);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`[PwaManager] ServiceWorker registration failed:`, error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Listen for controller changes (when skipWaiting activates new worker)
|
|
67
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
68
|
+
console.log('[PwaManager] ServiceWorker controller changed. Page ready for reload.');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Automatic Home Screen Badging Synchronization
|
|
72
|
+
if (this.options.enableBadgingSync && typeof window !== 'undefined') {
|
|
73
|
+
this.syncAppBadge();
|
|
74
|
+
|
|
75
|
+
window.addEventListener('focus', () => this.syncAppBadge());
|
|
76
|
+
document.addEventListener('visibilitychange', () => {
|
|
77
|
+
if (document.visibilityState === 'visible') {
|
|
78
|
+
this.syncAppBadge();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
83
|
+
if (event.data?.type === 'ON_APP_NOTIFICATION') {
|
|
84
|
+
this.syncAppBadge();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this.registration;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Drops legacy or misconfigured workers from older architecture iterations
|
|
94
|
+
*/
|
|
95
|
+
static async purgeLegacyWorkers() {
|
|
96
|
+
try {
|
|
97
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
98
|
+
for (const reg of registrations) {
|
|
99
|
+
const scriptURL = (reg.active || reg.waiting || reg.installing || {}).scriptURL || '';
|
|
100
|
+
// If worker is scoped to /public/ or named /service-worker.js, unregister it
|
|
101
|
+
if (scriptURL.endsWith('/public/sw.js') || scriptURL.endsWith('/service-worker.js')) {
|
|
102
|
+
console.warn('[PwaManager] Unregistering legacy orphaned worker:', scriptURL);
|
|
103
|
+
await reg.unregister();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('[PwaManager] Legacy cleanup check error:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handles update notification
|
|
113
|
+
*/
|
|
114
|
+
static handleUpdateAvailable(worker) {
|
|
115
|
+
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
|
116
|
+
window.dispatchEvent(new CustomEvent('pwa:update-available', {
|
|
117
|
+
detail: { worker, appName: this.options.appName }
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof this.options.onUpdateAvailable === 'function') {
|
|
122
|
+
this.options.onUpdateAvailable(worker);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sends skipWaiting postMessage to the waiting worker and triggers clean reload
|
|
128
|
+
*/
|
|
129
|
+
static applyUpdate(worker = null) {
|
|
130
|
+
const targetWorker = worker || (this.registration ? this.registration.waiting : null);
|
|
131
|
+
if (targetWorker) {
|
|
132
|
+
targetWorker.postMessage({ action: 'skipWaiting' });
|
|
133
|
+
} else if (navigator.serviceWorker.controller) {
|
|
134
|
+
navigator.serviceWorker.controller.postMessage({ action: 'skipWaiting' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
138
|
+
window.location.reload();
|
|
139
|
+
}, { once: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Purges sensitive API and user caches from the Service Worker upon user logout
|
|
144
|
+
*/
|
|
145
|
+
static async clearUserCacheOnLogout() {
|
|
146
|
+
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
|
147
|
+
navigator.serviceWorker.controller.postMessage({ action: 'clearUserCache' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof caches !== 'undefined') {
|
|
151
|
+
try {
|
|
152
|
+
const keys = await caches.keys();
|
|
153
|
+
await Promise.all(
|
|
154
|
+
keys.filter(k => k.includes('api-cache') || k.includes('user-cache')).map(k => caches.delete(k))
|
|
155
|
+
);
|
|
156
|
+
console.log('[PwaManager] Client-side sensitive user caches purged.');
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn('[PwaManager] Failed to purge user caches:', e);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sets app badge counter on the phone's home screen icon (Web Badging API)
|
|
165
|
+
*
|
|
166
|
+
* @param {number|string} count
|
|
167
|
+
* @returns {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
static setAppBadge(count) {
|
|
170
|
+
if (typeof navigator !== 'undefined' && 'setAppBadge' in navigator) {
|
|
171
|
+
const num = Math.max(0, parseInt(count, 10) || 0);
|
|
172
|
+
return (num > 0 ? navigator.setAppBadge(num) : navigator.clearAppBadge()).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
return Promise.resolve();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clears app badge counter from the phone's home screen icon
|
|
179
|
+
*
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
static clearAppBadge() {
|
|
183
|
+
if (typeof navigator !== 'undefined' && 'clearAppBadge' in navigator) {
|
|
184
|
+
return navigator.clearAppBadge().catch(() => {});
|
|
185
|
+
}
|
|
186
|
+
return Promise.resolve();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Synchronizes app badge with the server's unread notifications endpoint
|
|
191
|
+
*
|
|
192
|
+
* @param {string|null} endpoint
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
static async syncAppBadge(endpoint = null) {
|
|
196
|
+
const url = endpoint || this.options.badgeEndpoint || '/api/notifications/unread';
|
|
197
|
+
if (typeof navigator === 'undefined' || !('setAppBadge' in navigator)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
// Release the stream before bailing. An abandoned response body stays
|
|
205
|
+
// "in flight" in Chromium indefinitely, which keeps the tab from ever
|
|
206
|
+
// reaching network idle — it silently wedges Playwright's
|
|
207
|
+
// `networkidle` waits and any load-state detection for the whole page.
|
|
208
|
+
await res.body?.cancel();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const data = await res.json();
|
|
212
|
+
if (data && data.status === 'success' && Array.isArray(data.message)) {
|
|
213
|
+
const count = data.message.length;
|
|
214
|
+
if (count > 0) {
|
|
215
|
+
await this.setAppBadge(count);
|
|
216
|
+
} else {
|
|
217
|
+
await this.clearAppBadge();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
// Harmless no-op on network failure or offline mode
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/README.md
CHANGED
|
@@ -32,6 +32,80 @@ import { sanitizeInput } from '@modernman00/shared-js-lib';
|
|
|
32
32
|
const cleanInput = sanitizeInput('<script>alert("xss")</script>');
|
|
33
33
|
console.log(cleanInput); // Sanitized output
|
|
34
34
|
|
|
35
|
+
### Example: SweetAlert2 Notifications & Modals
|
|
36
|
+
```javascript
|
|
37
|
+
import { swalToast, swalSuccess, swalError, swalConfirm } from '@modernman00/shared-js-lib';
|
|
38
|
+
|
|
39
|
+
// Fast non-blocking toast at top-end
|
|
40
|
+
await swalToast('Profile updated successfully!');
|
|
41
|
+
|
|
42
|
+
// Prominent success or error modal
|
|
43
|
+
await swalSuccess('Your application has been submitted.', false);
|
|
44
|
+
await swalError('Payment processing failed. Please check your card.');
|
|
45
|
+
|
|
46
|
+
// Interactive confirmation dialog
|
|
47
|
+
const proceed = await swalConfirm('Delete Account', 'Are you sure? This cannot be undone.');
|
|
48
|
+
if (proceed) {
|
|
49
|
+
// perform deletion
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Example: Framework-Agnostic Button Spinners
|
|
54
|
+
```javascript
|
|
55
|
+
import { setButtonLoading, clearButtonLoading, withButtonSpinner } from '@modernman00/shared-js-lib';
|
|
56
|
+
|
|
57
|
+
// Manual trigger
|
|
58
|
+
setButtonLoading('submitButton'); // Disables button & shows smooth SVG spinner
|
|
59
|
+
// ... do work
|
|
60
|
+
clearButtonLoading('submitButton'); // Restores original text & enables button
|
|
61
|
+
|
|
62
|
+
// Automatic wrapper with guaranteed cleanup:
|
|
63
|
+
await withButtonSpinner('submitButton', async () => {
|
|
64
|
+
await apiCall();
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Example: Modern Form Submission (`postFormData`)
|
|
69
|
+
```javascript
|
|
70
|
+
import { postFormData } from '@modernman00/shared-js-lib';
|
|
71
|
+
|
|
72
|
+
// Modern clean syntax: auto-handles button spinner & SweetAlert2 without needing a notification element!
|
|
73
|
+
await postFormData('/event/update', 'glbVenueForm', {
|
|
74
|
+
button: '#glbVenueSaveBtn', // auto-spins & disables button, restores in finally{}
|
|
75
|
+
css: 'bootstrap', // or 'bulma', 'tailwind'
|
|
76
|
+
redirect: null // optional redirect URL
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Example: Advanced Enterprise Features
|
|
81
|
+
```javascript
|
|
82
|
+
import {
|
|
83
|
+
withSubmissionLock,
|
|
84
|
+
setupNetworkListeners,
|
|
85
|
+
attachInlineValidation,
|
|
86
|
+
copyToClipboard,
|
|
87
|
+
setupAuthExpiryInterceptor
|
|
88
|
+
} from '@modernman00/shared-js-lib';
|
|
89
|
+
|
|
90
|
+
// 1. Double Submit Protection Lock
|
|
91
|
+
await withSubmissionLock('payout_action', async () => {
|
|
92
|
+
await submitPayment();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 2. Automatic Online/Offline Connection Alerts
|
|
96
|
+
setupNetworkListeners(); // Automatically alerts user when connection drops or reconnects
|
|
97
|
+
|
|
98
|
+
// 3. Real-Time Inline Field Validation with Micro-Feedback
|
|
99
|
+
attachInlineValidation('userEmail', { type: 'email' });
|
|
100
|
+
attachInlineValidation('userPassword', { type: 'password' });
|
|
101
|
+
|
|
102
|
+
// 4. One-Click Copy-to-Clipboard with SweetAlert2 Toast
|
|
103
|
+
await copyToClipboard('INVITE-2026-XYZ', 'Invite code copied to clipboard!');
|
|
104
|
+
|
|
105
|
+
// 5. Global 401 Session Expiry Interceptor & Re-login Modal
|
|
106
|
+
setupAuthExpiryInterceptor({ loginUrl: '/auth/login' });
|
|
107
|
+
```
|
|
108
|
+
|
|
35
109
|
Configuration
|
|
36
110
|
|
|
37
111
|
Ensure your project uses ES Modules by adding "type": "module" to your package.json, or use a bundler like Laravel Mix/Webpack.
|