@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/ShareManager.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShareManager.js
|
|
3
|
+
* Universal Social Share Manager with native Web Share API and glassmorphic fallback modal.
|
|
4
|
+
* Part of @modernman00/shared-js-lib
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CardCanvasGenerator } from './CardCanvasGenerator.js';
|
|
8
|
+
|
|
9
|
+
export class ShareManager {
|
|
10
|
+
/**
|
|
11
|
+
* Smart share dispatcher: attempts native share if supported, otherwise opens the universal modal.
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} [options.title] - Share title
|
|
14
|
+
* @param {string} [options.text] - Share description / caption
|
|
15
|
+
* @param {string} [options.url] - URL to share (defaults to window.location.href)
|
|
16
|
+
* @param {Blob|File} [options.file] - Optional image file or blob to attach to native share
|
|
17
|
+
* @param {string} [options.imageUrl] - Preview image URL for the fallback modal
|
|
18
|
+
* @param {Object} [options.cardOptions] - Options to pass to CardCanvasGenerator if generating dynamically
|
|
19
|
+
* @param {boolean} [options.forceModal=false] - Force opening the modal even if native share is supported
|
|
20
|
+
* @returns {Promise<boolean>} True if shared or modal opened successfully
|
|
21
|
+
*/
|
|
22
|
+
static async share(options = {}) {
|
|
23
|
+
const url = options.url || (typeof window !== 'undefined' ? window.location.href : '');
|
|
24
|
+
const title = options.title || (typeof document !== 'undefined' ? document.title : 'Share');
|
|
25
|
+
const text = options.text || '';
|
|
26
|
+
|
|
27
|
+
// If forced modal or running in an environment without navigator.share
|
|
28
|
+
if (!options.forceModal && typeof navigator !== 'undefined' && typeof navigator.share === 'function') {
|
|
29
|
+
try {
|
|
30
|
+
const shareData = { title, text, url };
|
|
31
|
+
|
|
32
|
+
// Attach file if provided and supported
|
|
33
|
+
if (options.file && typeof navigator.canShare === 'function') {
|
|
34
|
+
const testFile = options.file instanceof File
|
|
35
|
+
? options.file
|
|
36
|
+
: new File([options.file], 'share-card.png', { type: options.file.type || 'image/png' });
|
|
37
|
+
|
|
38
|
+
if (navigator.canShare({ files: [testFile] })) {
|
|
39
|
+
shareData.files = [testFile];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await navigator.share(shareData);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// If user cancelled, don't force modal; return false
|
|
47
|
+
if (err && (err.name === 'AbortError' || err.message?.includes('abort') || err.message?.includes('cancel'))) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// If native share failed for another reason, fallback to modal
|
|
51
|
+
console.warn('[ShareManager] Native share failed, falling back to modal:', err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this.openModal({ ...options, url, title, text });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Opens the glassmorphic Universal Share Modal
|
|
60
|
+
* @param {Object} options
|
|
61
|
+
* @returns {HTMLElement} The modal DOM element
|
|
62
|
+
*/
|
|
63
|
+
static openModal(options = {}) {
|
|
64
|
+
if (typeof document === 'undefined') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const modalId = 'shared-lib-universal-share-modal';
|
|
69
|
+
let modal = document.getElementById(modalId);
|
|
70
|
+
if (!modal) {
|
|
71
|
+
modal = this._createModalElement(modalId);
|
|
72
|
+
document.body.appendChild(modal);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this._populateModal(modal, options);
|
|
76
|
+
|
|
77
|
+
// Show modal with smooth fade
|
|
78
|
+
modal.style.display = 'flex';
|
|
79
|
+
requestAnimationFrame(() => {
|
|
80
|
+
modal.style.opacity = '1';
|
|
81
|
+
const dialog = modal.querySelector('.share-dialog');
|
|
82
|
+
if (dialog) {
|
|
83
|
+
dialog.style.transform = 'scale(1) translateY(0)';
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return modal;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Closes the active share modal
|
|
92
|
+
*/
|
|
93
|
+
static closeModal() {
|
|
94
|
+
if (typeof document === 'undefined') return;
|
|
95
|
+
const modal = document.getElementById('shared-lib-universal-share-modal');
|
|
96
|
+
if (!modal) return;
|
|
97
|
+
|
|
98
|
+
modal.style.opacity = '0';
|
|
99
|
+
const dialog = modal.querySelector('.share-dialog');
|
|
100
|
+
if (dialog) {
|
|
101
|
+
dialog.style.transform = 'scale(0.96) translateY(12px)';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
modal.style.display = 'none';
|
|
106
|
+
}, 200);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Copies an image Blob to the user's system clipboard using ClipboardItem
|
|
111
|
+
* @param {Blob} imageBlob
|
|
112
|
+
* @returns {Promise<boolean>}
|
|
113
|
+
*/
|
|
114
|
+
static async copyImageToClipboard(imageBlob) {
|
|
115
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard || !window.ClipboardItem) {
|
|
116
|
+
throw new Error('ClipboardItem image copying is not supported in this browser.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const item = new window.ClipboardItem({ [imageBlob.type || 'image/png']: imageBlob });
|
|
120
|
+
await navigator.clipboard.write([item]);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Downloads a Blob or DataURL as a file
|
|
126
|
+
* @param {Blob|string} source
|
|
127
|
+
* @param {string} [filename='share-card.png']
|
|
128
|
+
*/
|
|
129
|
+
static downloadFile(source, filename = 'share-card.png') {
|
|
130
|
+
if (typeof document === 'undefined') return;
|
|
131
|
+
const link = document.createElement('a');
|
|
132
|
+
let url = '';
|
|
133
|
+
|
|
134
|
+
if (source instanceof Blob) {
|
|
135
|
+
url = typeof URL.createObjectURL === 'function' ? URL.createObjectURL(source) : '';
|
|
136
|
+
} else {
|
|
137
|
+
url = source;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
link.href = url;
|
|
141
|
+
link.download = filename;
|
|
142
|
+
document.body.appendChild(link);
|
|
143
|
+
link.click();
|
|
144
|
+
document.body.removeChild(link);
|
|
145
|
+
|
|
146
|
+
if (source instanceof Blob && typeof URL.revokeObjectURL === 'function') {
|
|
147
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Internal DOM & Modal Construction ---
|
|
152
|
+
|
|
153
|
+
static _createModalElement(id) {
|
|
154
|
+
const modal = document.createElement('div');
|
|
155
|
+
modal.id = id;
|
|
156
|
+
modal.setAttribute('role', 'dialog');
|
|
157
|
+
modal.setAttribute('aria-modal', 'true');
|
|
158
|
+
modal.setAttribute('aria-label', 'Share');
|
|
159
|
+
|
|
160
|
+
// Overlay styles
|
|
161
|
+
modal.style.position = 'fixed';
|
|
162
|
+
modal.style.top = '0';
|
|
163
|
+
modal.style.left = '0';
|
|
164
|
+
modal.style.width = '100vw';
|
|
165
|
+
modal.style.height = '100vh';
|
|
166
|
+
modal.style.backgroundColor = 'rgba(5, 8, 16, 0.75)';
|
|
167
|
+
modal.style.backdropFilter = 'blur(10px)';
|
|
168
|
+
modal.style.webkitBackdropFilter = 'blur(10px)';
|
|
169
|
+
modal.style.zIndex = '99999';
|
|
170
|
+
modal.style.display = 'none';
|
|
171
|
+
modal.style.alignItems = 'center';
|
|
172
|
+
modal.style.justifyContent = 'center';
|
|
173
|
+
modal.style.opacity = '0';
|
|
174
|
+
modal.style.transition = 'opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1)';
|
|
175
|
+
modal.style.padding = '16px';
|
|
176
|
+
modal.style.boxSizing = 'border-box';
|
|
177
|
+
|
|
178
|
+
// Click backdrop to close
|
|
179
|
+
modal.addEventListener('click', (e) => {
|
|
180
|
+
if (e.target === modal) {
|
|
181
|
+
this.closeModal();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ESC key to close
|
|
186
|
+
document.addEventListener('keydown', (e) => {
|
|
187
|
+
if (e.key === 'Escape' && modal.style.display !== 'none') {
|
|
188
|
+
this.closeModal();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return modal;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static _populateModal(modal, options) {
|
|
196
|
+
const url = options.url || window.location.href;
|
|
197
|
+
const title = options.title || document.title;
|
|
198
|
+
const encodedUrl = encodeURIComponent(url);
|
|
199
|
+
const encodedTitle = encodeURIComponent(title);
|
|
200
|
+
|
|
201
|
+
// Escape user-supplied strings for safety
|
|
202
|
+
const safeTitle = this._escapeHtml(title);
|
|
203
|
+
|
|
204
|
+
modal.innerHTML = `
|
|
205
|
+
<div class="share-dialog" style="
|
|
206
|
+
background: #0f172a;
|
|
207
|
+
color: #ffffff;
|
|
208
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
209
|
+
border-radius: 20px;
|
|
210
|
+
max-width: 580px;
|
|
211
|
+
width: 100%;
|
|
212
|
+
max-height: 90vh;
|
|
213
|
+
overflow-y: auto;
|
|
214
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
|
|
215
|
+
padding: 24px;
|
|
216
|
+
box-sizing: border-box;
|
|
217
|
+
transform: scale(0.96) translateY(12px);
|
|
218
|
+
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
219
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
220
|
+
">
|
|
221
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
222
|
+
<div>
|
|
223
|
+
<h3 style="margin: 0; font-size: 1.25rem; font-weight: 700; color: #ffffff;">Share</h3>
|
|
224
|
+
<p style="margin: 4px 0 0 0; font-size: 0.85rem; color: #94a3b8;">Distribute graphics & link with your network</p>
|
|
225
|
+
</div>
|
|
226
|
+
<button type="button" class="btn-close-modal" aria-label="Close" style="
|
|
227
|
+
background: rgba(255, 255, 255, 0.08);
|
|
228
|
+
border: none;
|
|
229
|
+
color: #94a3b8;
|
|
230
|
+
width: 44px;
|
|
231
|
+
height: 44px;
|
|
232
|
+
border-radius: 50%;
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
font-size: 1.25rem;
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
transition: all 0.15s ease;
|
|
239
|
+
">×</button>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="card-preview-area" style="
|
|
243
|
+
border-radius: 12px;
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
margin-bottom: 20px;
|
|
246
|
+
background: #090d16;
|
|
247
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
min-height: 180px;
|
|
252
|
+
">
|
|
253
|
+
<!-- Live Canvas or Image Preview rendered below -->
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;">
|
|
257
|
+
<button type="button" class="btn-action-download" style="
|
|
258
|
+
background: rgba(255, 255, 255, 0.05);
|
|
259
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
260
|
+
border-radius: 10px;
|
|
261
|
+
color: #ffffff;
|
|
262
|
+
padding: 10px 8px;
|
|
263
|
+
font-size: 0.8rem;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
min-height: 44px;
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
justify-content: center;
|
|
270
|
+
gap: 6px;
|
|
271
|
+
">
|
|
272
|
+
<span>⬇ Download Card</span>
|
|
273
|
+
</button>
|
|
274
|
+
<button type="button" class="btn-action-copy-img" style="
|
|
275
|
+
background: rgba(255, 255, 255, 0.05);
|
|
276
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
277
|
+
border-radius: 10px;
|
|
278
|
+
color: #ffffff;
|
|
279
|
+
padding: 10px 8px;
|
|
280
|
+
font-size: 0.8rem;
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
min-height: 44px;
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
justify-content: center;
|
|
287
|
+
gap: 6px;
|
|
288
|
+
">
|
|
289
|
+
<span>🖼 Copy Image</span>
|
|
290
|
+
</button>
|
|
291
|
+
<button type="button" class="btn-action-copy-link" style="
|
|
292
|
+
background: rgba(16, 185, 129, 0.15);
|
|
293
|
+
border: 1px solid rgba(16, 185, 129, 0.4);
|
|
294
|
+
border-radius: 10px;
|
|
295
|
+
color: #10b981;
|
|
296
|
+
padding: 10px 8px;
|
|
297
|
+
font-size: 0.8rem;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
min-height: 44px;
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
justify-content: center;
|
|
304
|
+
gap: 6px;
|
|
305
|
+
">
|
|
306
|
+
<span class="copy-link-text">🔗 Copy Link</span>
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div style="font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; font-weight: 700; margin-bottom: 10px;">
|
|
311
|
+
Share directly to platforms
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div style="display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px;">
|
|
315
|
+
<a href="https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}" target="_blank" rel="noopener noreferrer" style="
|
|
316
|
+
background: #0a66c2; color: #fff; text-decoration: none; border-radius: 10px; min-height: 48px;
|
|
317
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 600;
|
|
318
|
+
">LinkedIn</a>
|
|
319
|
+
|
|
320
|
+
<a href="https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}" target="_blank" rel="noopener noreferrer" style="
|
|
321
|
+
background: #000000; color: #fff; text-decoration: none; border: 1px solid rgba(255,255,255,0.2); border-radius: 10px; min-height: 48px;
|
|
322
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 600;
|
|
323
|
+
">X / Twitter</a>
|
|
324
|
+
|
|
325
|
+
<a href="https://api.whatsapp.com/send?text=${encodedTitle}%20${encodedUrl}" target="_blank" rel="noopener noreferrer" style="
|
|
326
|
+
background: #25d366; color: #fff; text-decoration: none; border-radius: 10px; min-height: 48px;
|
|
327
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 600;
|
|
328
|
+
">WhatsApp</a>
|
|
329
|
+
|
|
330
|
+
<a href="https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}" target="_blank" rel="noopener noreferrer" style="
|
|
331
|
+
background: #1877f2; color: #fff; text-decoration: none; border-radius: 10px; min-height: 48px;
|
|
332
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 600;
|
|
333
|
+
">Facebook</a>
|
|
334
|
+
|
|
335
|
+
<a href="viber://forward?text=${encodedTitle}%20${encodedUrl}" style="
|
|
336
|
+
background: #7360f2; color: #fff; text-decoration: none; border-radius: 10px; min-height: 48px;
|
|
337
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 600;
|
|
338
|
+
">Viber</a>
|
|
339
|
+
|
|
340
|
+
<a href="sms:?&body=${encodedTitle}%20${encodedUrl}" style="
|
|
341
|
+
background: #334155; color: #fff; text-decoration: none; border-radius: 10px; min-height: 48px;
|
|
342
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 600;
|
|
343
|
+
">SMS</a>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
// Hook Close Button
|
|
349
|
+
const closeBtn = modal.querySelector('.btn-close-modal');
|
|
350
|
+
if (closeBtn) {
|
|
351
|
+
closeBtn.onclick = () => this.closeModal();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Hook Preview & Actions
|
|
355
|
+
const previewContainer = modal.querySelector('.card-preview-area');
|
|
356
|
+
let activeBlob = options.file instanceof Blob ? options.file : null;
|
|
357
|
+
|
|
358
|
+
if (options.cardOptions) {
|
|
359
|
+
// Live dynamic canvas rendering
|
|
360
|
+
const canvas = document.createElement('canvas');
|
|
361
|
+
canvas.style.width = '100%';
|
|
362
|
+
canvas.style.height = 'auto';
|
|
363
|
+
canvas.style.display = 'block';
|
|
364
|
+
previewContainer.innerHTML = '';
|
|
365
|
+
previewContainer.appendChild(canvas);
|
|
366
|
+
CardCanvasGenerator.renderToCanvas(canvas, options.cardOptions);
|
|
367
|
+
canvas.toBlob((b) => {
|
|
368
|
+
activeBlob = b;
|
|
369
|
+
}, 'image/png');
|
|
370
|
+
} else if (options.imageUrl) {
|
|
371
|
+
const img = document.createElement('img');
|
|
372
|
+
img.src = options.imageUrl;
|
|
373
|
+
img.alt = safeTitle;
|
|
374
|
+
img.style.maxWidth = '100%';
|
|
375
|
+
img.style.height = 'auto';
|
|
376
|
+
img.style.display = 'block';
|
|
377
|
+
previewContainer.innerHTML = '';
|
|
378
|
+
previewContainer.appendChild(img);
|
|
379
|
+
} else {
|
|
380
|
+
previewContainer.innerHTML = `<div style="padding: 24px; text-align: center; color: #94a3b8; font-size: 0.85rem;">Ready to share: <strong>${safeTitle}</strong></div>`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Hook Download Action
|
|
384
|
+
const dlBtn = modal.querySelector('.btn-action-download');
|
|
385
|
+
if (dlBtn) {
|
|
386
|
+
dlBtn.onclick = async () => {
|
|
387
|
+
if (activeBlob) {
|
|
388
|
+
this.downloadFile(activeBlob, 'share-card.png');
|
|
389
|
+
} else if (options.imageUrl) {
|
|
390
|
+
this.downloadFile(options.imageUrl, 'share-card.png');
|
|
391
|
+
} else if (options.cardOptions) {
|
|
392
|
+
const b = await CardCanvasGenerator.generateBlob(options.cardOptions);
|
|
393
|
+
this.downloadFile(b, 'share-card.png');
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Hook Copy Image Action
|
|
399
|
+
const copyImgBtn = modal.querySelector('.btn-action-copy-img');
|
|
400
|
+
if (copyImgBtn) {
|
|
401
|
+
copyImgBtn.onclick = async () => {
|
|
402
|
+
try {
|
|
403
|
+
let b = activeBlob;
|
|
404
|
+
if (!b && options.cardOptions) {
|
|
405
|
+
b = await CardCanvasGenerator.generateBlob(options.cardOptions);
|
|
406
|
+
}
|
|
407
|
+
if (b) {
|
|
408
|
+
await this.copyImageToClipboard(b);
|
|
409
|
+
copyImgBtn.innerHTML = '<span>✅ Copied!</span>';
|
|
410
|
+
setTimeout(() => {
|
|
411
|
+
copyImgBtn.innerHTML = '<span>🖼 Copy Image</span>';
|
|
412
|
+
}, 2000);
|
|
413
|
+
} else {
|
|
414
|
+
alert('Image preview not ready to copy.');
|
|
415
|
+
}
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.warn('[ShareManager] Copy image failed:', err);
|
|
418
|
+
alert('Copy image is not supported in this browser. Please use Download Card instead.');
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Hook Copy Link Action
|
|
424
|
+
const copyLinkBtn = modal.querySelector('.btn-action-copy-link');
|
|
425
|
+
const copyLinkText = modal.querySelector('.copy-link-text');
|
|
426
|
+
if (copyLinkBtn) {
|
|
427
|
+
copyLinkBtn.onclick = async () => {
|
|
428
|
+
try {
|
|
429
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
430
|
+
await navigator.clipboard.writeText(url);
|
|
431
|
+
} else {
|
|
432
|
+
const input = document.createElement('input');
|
|
433
|
+
input.value = url;
|
|
434
|
+
document.body.appendChild(input);
|
|
435
|
+
input.select();
|
|
436
|
+
document.execCommand('copy');
|
|
437
|
+
document.body.removeChild(input);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (copyLinkText) {
|
|
441
|
+
copyLinkText.textContent = '✅ Copied!';
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
copyLinkText.textContent = '🔗 Copy Link';
|
|
444
|
+
}, 2000);
|
|
445
|
+
}
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.warn('[ShareManager] Copy link failed:', err);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static _escapeHtml(str) {
|
|
454
|
+
if (!str) return '';
|
|
455
|
+
return String(str)
|
|
456
|
+
.replace(/&/g, '&')
|
|
457
|
+
.replace(/</g, '<')
|
|
458
|
+
.replace(/>/g, '>')
|
|
459
|
+
.replace(/"/g, '"')
|
|
460
|
+
.replace(/'/g, ''');
|
|
461
|
+
}
|
|
462
|
+
}
|
package/ShowResponse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { id, qSel } from './UtilityHtml.js';
|
|
2
|
+
import { swalToast, swalSuccess, swalError } from './Alert.js';
|
|
2
3
|
|
|
3
4
|
export const showError = (e) =>
|
|
4
5
|
console.error(
|
|
@@ -9,6 +10,23 @@ export const msgException = (errorMessage) => {
|
|
|
9
10
|
throw new Error(errorMessage);
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Drop-in SweetAlert-backed notification.
|
|
15
|
+
* If status is true or 'green', shows success toast; otherwise shows error toast/modal.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} message - Message to display
|
|
18
|
+
* @param {boolean|string} [status=true] - true/'green' for success, false/'red' for error
|
|
19
|
+
* @param {boolean} [asToast=true] - true to show as sleek toast, false for modal
|
|
20
|
+
* @returns {Promise<any>}
|
|
21
|
+
*/
|
|
22
|
+
export const swalNotification = async (message, status = true, asToast = true) => {
|
|
23
|
+
const isSuccess = status === true || status === 'green' || status === 'is-success' || status === 'w3-green';
|
|
24
|
+
if (isSuccess) {
|
|
25
|
+
return await swalSuccess(message, asToast);
|
|
26
|
+
}
|
|
27
|
+
return await swalError(message, !asToast);
|
|
28
|
+
};
|
|
29
|
+
|
|
12
30
|
/**
|
|
13
31
|
*
|
|
14
32
|
* @param {*} elementId - element id
|
|
@@ -55,3 +73,105 @@ export const showResponse = (theId, message, status) => {
|
|
|
55
73
|
responseEl.innerHTML = '';
|
|
56
74
|
}, 5000); // Disappear after 5 seconds
|
|
57
75
|
};
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Defensive Error Unwrapping Utility
|
|
80
|
+
*
|
|
81
|
+
* Extracts a human-readable string message from any error shape
|
|
82
|
+
* (string, Error object, Axios/Fetch HTTP response, validation map).
|
|
83
|
+
* Mathematically guarantees that [object Object] is NEVER returned.
|
|
84
|
+
*
|
|
85
|
+
* @param {any} err - The error object, string, or HTTP response error
|
|
86
|
+
* @param {string} defaultMessage - Fallback message if extraction yields empty
|
|
87
|
+
* @returns {string} Clean human-readable error text
|
|
88
|
+
*/
|
|
89
|
+
export const extractErrorMessage = (err, defaultMessage = "An unexpected error occurred.") => {
|
|
90
|
+
if (err === null || err === undefined) {
|
|
91
|
+
return defaultMessage;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof err === "string") {
|
|
95
|
+
const trimmed = err.trim();
|
|
96
|
+
return trimmed && trimmed !== "[object Object]" ? trimmed : defaultMessage;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 1. Check Axios / HTTP response envelope (err.response.data)
|
|
100
|
+
if (err && typeof err === "object") {
|
|
101
|
+
const resData = err.response ? err.response.data : (err.data || null);
|
|
102
|
+
|
|
103
|
+
if (resData) {
|
|
104
|
+
if (typeof resData === "string") {
|
|
105
|
+
const trimmed = resData.trim();
|
|
106
|
+
if (trimmed && trimmed !== "[object Object]") return trimmed;
|
|
107
|
+
} else if (typeof resData === "object") {
|
|
108
|
+
// Check standard envelope fields: error, message, msg
|
|
109
|
+
if (typeof resData.error === "string" && resData.error.trim() && resData.error.trim() !== "[object Object]") {
|
|
110
|
+
return resData.error.trim();
|
|
111
|
+
}
|
|
112
|
+
if (typeof resData.message === "string" && resData.message.trim() && resData.message.trim() !== "[object Object]") {
|
|
113
|
+
return resData.message.trim();
|
|
114
|
+
}
|
|
115
|
+
if (typeof resData.msg === "string" && resData.msg.trim() && resData.msg.trim() !== "[object Object]") {
|
|
116
|
+
return resData.msg.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check validation errors map (e.g. { errors: { email: ["Invalid"], password: ["Too short"] } })
|
|
120
|
+
const errorsSource = resData.errors || resData.error;
|
|
121
|
+
if (errorsSource && typeof errorsSource === "object") {
|
|
122
|
+
if (Array.isArray(errorsSource)) {
|
|
123
|
+
const msgs = errorsSource.map(m => typeof m === "string" ? m : (m?.message || JSON.stringify(m))).filter(Boolean);
|
|
124
|
+
if (msgs.length > 0) return msgs.join(", ");
|
|
125
|
+
} else {
|
|
126
|
+
const values = Object.values(errorsSource).flat().map(v => typeof v === "string" ? v : (v?.message || JSON.stringify(v))).filter(Boolean);
|
|
127
|
+
if (values.length > 0) return values.join(", ");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Check standard Error instance message
|
|
134
|
+
if (typeof err.message === "string" && err.message.trim() && err.message.trim() !== "[object Object]") {
|
|
135
|
+
return err.message.trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 3. Check direct error/msg properties on object
|
|
139
|
+
if (typeof err.error === "string" && err.error.trim() && err.error.trim() !== "[object Object]") {
|
|
140
|
+
return err.error.trim();
|
|
141
|
+
}
|
|
142
|
+
if (typeof err.msg === "string" && err.msg.trim() && err.msg.trim() !== "[object Object]") {
|
|
143
|
+
return err.msg.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. Check errors map directly on object
|
|
147
|
+
if (err.errors && typeof err.errors === "object") {
|
|
148
|
+
if (Array.isArray(err.errors)) {
|
|
149
|
+
const msgs = err.errors.map(m => typeof m === "string" ? m : (m?.message || JSON.stringify(m))).filter(Boolean);
|
|
150
|
+
if (msgs.length > 0) return msgs.join(", ");
|
|
151
|
+
} else {
|
|
152
|
+
const values = Object.values(err.errors).flat().map(v => typeof v === "string" ? v : (v?.message || JSON.stringify(v))).filter(Boolean);
|
|
153
|
+
if (values.length > 0) return values.join(", ");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 5. Check custom toString if not default Object toString
|
|
158
|
+
try {
|
|
159
|
+
if (typeof err.toString === "function") {
|
|
160
|
+
const str = err.toString();
|
|
161
|
+
if (str && str !== "[object Object]" && typeof str === "string") {
|
|
162
|
+
return str;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (_) {}
|
|
166
|
+
|
|
167
|
+
// 6. Safe JSON stringify fallback
|
|
168
|
+
try {
|
|
169
|
+
const jsonStr = JSON.stringify(err);
|
|
170
|
+
if (jsonStr && jsonStr !== "{}" && jsonStr.length < 200) {
|
|
171
|
+
return jsonStr;
|
|
172
|
+
}
|
|
173
|
+
} catch (_) {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return defaultMessage;
|
|
177
|
+
};
|
package/axiosWrapper.js
CHANGED
|
@@ -4,6 +4,9 @@ import axiosRetry from 'axios-retry';
|
|
|
4
4
|
|
|
5
5
|
axiosRetry(axios, { retries: 3 });
|
|
6
6
|
|
|
7
|
+
// Ensure Axios always sends cookies (like the HttpOnly auth token) with every request
|
|
8
|
+
axios.defaults.withCredentials = true;
|
|
9
|
+
|
|
7
10
|
axios.interceptors.request.use(config => {
|
|
8
11
|
config.headers['X-Requested-With'] = 'XMLHttpRequest';
|
|
9
12
|
|
package/config/version.php
CHANGED
package/index.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
export * from './OfflineQueue.js';
|
|
2
|
+
export * from './PwaManager.js';
|
|
3
|
+
export * from './PwaInstall.js';
|
|
1
4
|
export * from './Http.js';
|
|
2
5
|
export * from './UtilityHtml.js';
|
|
3
6
|
export * from './Utility.js';
|
|
4
7
|
export * from './Loader.js';
|
|
8
|
+
export * from './Alert.js';
|
|
9
|
+
export * from './ButtonLoader.js';
|
|
10
|
+
export * from './FormLock.js';
|
|
11
|
+
export * from './NetworkGuard.js';
|
|
12
|
+
export * from './InlineValidation.js';
|
|
13
|
+
export * from './Clipboard.js';
|
|
14
|
+
export * from './AuthInterceptor.js';
|
|
5
15
|
export * from './Cookie.js';
|
|
6
16
|
export * from './FormHelper.js'
|
|
7
17
|
export * from './ShowResponse.js';
|
|
@@ -15,4 +25,7 @@ export * from './AcctMgt/processAll.js';
|
|
|
15
25
|
export * from './AcctMgt/update.js';
|
|
16
26
|
export * from './AcctMgt/registerHandler.js';
|
|
17
27
|
export * from './AcctMgt/recaptchaToken.js';
|
|
18
|
-
export * from './
|
|
28
|
+
export * from './AcctMgt/AdminAuthForm.js';
|
|
29
|
+
export * from './general.js';
|
|
30
|
+
export * from './CardCanvasGenerator.js';
|
|
31
|
+
export * from './ShareManager.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modernman00/shared-js-lib",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.83",
|
|
4
4
|
"description": "Reusable JS utilities for numerous js problems",
|
|
5
5
|
"homepage": "https://github.com/modernman00/shared-js-lib#readme",
|
|
6
6
|
"keywords": [
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"axios": "^1.13.2",
|
|
27
27
|
"axios-retry": "^4.5.0",
|
|
28
|
-
"settings": "^0.1.1"
|
|
28
|
+
"settings": "^0.1.1",
|
|
29
|
+
"sweetalert2": "^11.26.25"
|
|
29
30
|
},
|
|
30
31
|
"publishConfig": {
|
|
31
32
|
"access": "public"
|
|
@@ -40,6 +41,13 @@
|
|
|
40
41
|
"FormHelper.js",
|
|
41
42
|
"Http.js",
|
|
42
43
|
"Loader.js",
|
|
44
|
+
"Alert.js",
|
|
45
|
+
"ButtonLoader.js",
|
|
46
|
+
"FormLock.js",
|
|
47
|
+
"NetworkGuard.js",
|
|
48
|
+
"InlineValidation.js",
|
|
49
|
+
"Clipboard.js",
|
|
50
|
+
"AuthInterceptor.js",
|
|
43
51
|
"ShowResponse.js",
|
|
44
52
|
"Utility.js",
|
|
45
53
|
"UtilityHtml.js",
|
|
@@ -48,11 +56,18 @@
|
|
|
48
56
|
"general.js",
|
|
49
57
|
"config/",
|
|
50
58
|
"README.md",
|
|
51
|
-
"release-history.md"
|
|
59
|
+
"release-history.md",
|
|
60
|
+
"OfflineQueue.js",
|
|
61
|
+
"PwaManager.js",
|
|
62
|
+
"PwaInstall.js",
|
|
63
|
+
"CardCanvasGenerator.js",
|
|
64
|
+
"ShareManager.js",
|
|
65
|
+
"templates/"
|
|
52
66
|
],
|
|
53
67
|
"devDependencies": {
|
|
54
68
|
"@babel/core": "^7.28.3",
|
|
55
69
|
"@babel/preset-env": "^7.28.3",
|
|
70
|
+
"@babel/runtime": "^8.0.5",
|
|
56
71
|
"@semantic-release/changelog": "^6.0.3",
|
|
57
72
|
"@semantic-release/git": "^10.0.1",
|
|
58
73
|
"@semantic-release/github": "^11.0.4",
|