@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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CardCanvasGenerator.js
|
|
3
|
+
* High-resolution 1200x630 / 1200x675 HTML5 Canvas Social Card Generator.
|
|
4
|
+
* Part of @modernman00/shared-js-lib
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class CardCanvasGenerator {
|
|
8
|
+
/**
|
|
9
|
+
* Default configuration options
|
|
10
|
+
*/
|
|
11
|
+
static get DEFAULT_OPTIONS() {
|
|
12
|
+
return {
|
|
13
|
+
width: 1200,
|
|
14
|
+
height: 675,
|
|
15
|
+
brandName: 'Platform',
|
|
16
|
+
brandBadge: 'DECISION INTELLIGENCE',
|
|
17
|
+
title: 'Strategic Analysis',
|
|
18
|
+
subtitle: '',
|
|
19
|
+
score: 85,
|
|
20
|
+
maxScore: 100,
|
|
21
|
+
scoreLabel: 'SCORE',
|
|
22
|
+
verdict: 'STRONG GO',
|
|
23
|
+
verdictColor: '#10b981', // emerald
|
|
24
|
+
primaryColor: '#10b981',
|
|
25
|
+
secondaryColor: '#3b82f6',
|
|
26
|
+
accentColor: '#6366f1',
|
|
27
|
+
backgroundColor: '#090d16',
|
|
28
|
+
cardBackground: 'rgba(255, 255, 255, 0.03)',
|
|
29
|
+
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
30
|
+
textColor: '#ffffff',
|
|
31
|
+
mutedTextColor: '#94a3b8',
|
|
32
|
+
watermarkText: 'Generated with Decision Intelligence Platform',
|
|
33
|
+
metrics: [], // Array of { label: string, score: number, maxScore?: number }
|
|
34
|
+
showGauge: true,
|
|
35
|
+
showMetrics: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Renders the card onto a target HTML5 canvas element
|
|
41
|
+
* @param {HTMLCanvasElement} canvas
|
|
42
|
+
* @param {Object} options
|
|
43
|
+
* @returns {HTMLCanvasElement}
|
|
44
|
+
*/
|
|
45
|
+
static renderToCanvas(canvas, options = {}) {
|
|
46
|
+
const opts = { ...this.DEFAULT_OPTIONS, ...options };
|
|
47
|
+
const width = opts.width;
|
|
48
|
+
const height = opts.height;
|
|
49
|
+
|
|
50
|
+
canvas.width = width;
|
|
51
|
+
canvas.height = height;
|
|
52
|
+
const ctx = canvas.getContext('2d');
|
|
53
|
+
if (!ctx) {
|
|
54
|
+
throw new Error('Unable to obtain 2D canvas rendering context.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 1. Draw Deep Gradient Background
|
|
58
|
+
const bgGrad = ctx.createLinearGradient(0, 0, width, height);
|
|
59
|
+
bgGrad.addColorStop(0, opts.backgroundColor || '#090d16');
|
|
60
|
+
bgGrad.addColorStop(0.5, '#0d1527');
|
|
61
|
+
bgGrad.addColorStop(1, '#050811');
|
|
62
|
+
ctx.fillStyle = bgGrad;
|
|
63
|
+
ctx.fillRect(0, 0, width, height);
|
|
64
|
+
|
|
65
|
+
// 2. Ambient Lighting / Glow Orbs
|
|
66
|
+
this._drawGlowOrb(ctx, 150, 120, 250, opts.primaryColor, 0.18);
|
|
67
|
+
this._drawGlowOrb(ctx, width - 200, height - 150, 300, opts.secondaryColor, 0.15);
|
|
68
|
+
|
|
69
|
+
// 3. Subtle Grid Pattern
|
|
70
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.025)';
|
|
71
|
+
ctx.lineWidth = 1;
|
|
72
|
+
for (let x = 40; x < width; x += 60) {
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
ctx.moveTo(x, 0);
|
|
75
|
+
ctx.lineTo(x, height);
|
|
76
|
+
ctx.stroke();
|
|
77
|
+
}
|
|
78
|
+
for (let y = 40; y < height; y += 60) {
|
|
79
|
+
ctx.beginPath();
|
|
80
|
+
ctx.moveTo(0, y);
|
|
81
|
+
ctx.lineTo(width, y);
|
|
82
|
+
ctx.stroke();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. Main Glassmorphic Card Container
|
|
86
|
+
const margin = 40;
|
|
87
|
+
const cardW = width - margin * 2;
|
|
88
|
+
const cardH = height - margin * 2;
|
|
89
|
+
this._drawRoundedRect(ctx, margin, margin, cardW, cardH, 24);
|
|
90
|
+
ctx.fillStyle = opts.cardBackground;
|
|
91
|
+
ctx.fill();
|
|
92
|
+
ctx.strokeStyle = opts.borderColor;
|
|
93
|
+
ctx.lineWidth = 1.5;
|
|
94
|
+
ctx.stroke();
|
|
95
|
+
|
|
96
|
+
// 5. Header: Brand & Badge
|
|
97
|
+
ctx.font = 'bold 26px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
98
|
+
ctx.fillStyle = opts.textColor;
|
|
99
|
+
ctx.textAlign = 'left';
|
|
100
|
+
ctx.textBaseline = 'middle';
|
|
101
|
+
ctx.fillText(opts.brandName, margin + 40, margin + 50);
|
|
102
|
+
|
|
103
|
+
if (opts.brandBadge) {
|
|
104
|
+
const brandWidth = ctx.measureText(opts.brandName).width;
|
|
105
|
+
const badgeX = margin + 40 + brandWidth + 16;
|
|
106
|
+
const badgeY = margin + 37;
|
|
107
|
+
const badgeText = String(opts.brandBadge).toUpperCase();
|
|
108
|
+
ctx.font = 'bold 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
109
|
+
const badgeW = ctx.measureText(badgeText).width + 24;
|
|
110
|
+
|
|
111
|
+
this._drawRoundedRect(ctx, badgeX, badgeY, badgeW, 26, 6);
|
|
112
|
+
ctx.fillStyle = 'rgba(16, 185, 129, 0.15)';
|
|
113
|
+
ctx.fill();
|
|
114
|
+
ctx.strokeStyle = 'rgba(16, 185, 129, 0.4)';
|
|
115
|
+
ctx.lineWidth = 1;
|
|
116
|
+
ctx.stroke();
|
|
117
|
+
|
|
118
|
+
ctx.fillStyle = opts.primaryColor;
|
|
119
|
+
ctx.fillText(badgeText, badgeX + 12, badgeY + 13);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Header Divider Line
|
|
123
|
+
ctx.beginPath();
|
|
124
|
+
ctx.moveTo(margin + 40, margin + 85);
|
|
125
|
+
ctx.lineTo(margin + cardW - 40, margin + 85);
|
|
126
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
|
|
127
|
+
ctx.lineWidth = 1;
|
|
128
|
+
ctx.stroke();
|
|
129
|
+
|
|
130
|
+
// 6. Left Column: Circular Glowing Gauge (if enabled)
|
|
131
|
+
const hasGauge = Boolean(opts.showGauge);
|
|
132
|
+
const leftColWidth = hasGauge ? 340 : 0;
|
|
133
|
+
const contentStartX = margin + 40 + leftColWidth;
|
|
134
|
+
|
|
135
|
+
if (hasGauge) {
|
|
136
|
+
const gaugeCenterX = margin + 170;
|
|
137
|
+
const gaugeCenterY = margin + 280;
|
|
138
|
+
const gaugeRadius = 110;
|
|
139
|
+
|
|
140
|
+
// Outer track
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
ctx.arc(gaugeCenterX, gaugeCenterY, gaugeRadius, 0, Math.PI * 2);
|
|
143
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
|
|
144
|
+
ctx.lineWidth = 16;
|
|
145
|
+
ctx.lineCap = 'round';
|
|
146
|
+
ctx.stroke();
|
|
147
|
+
|
|
148
|
+
// Active Arc
|
|
149
|
+
const scorePct = Math.max(0, Math.min(100, opts.score)) / (opts.maxScore || 100);
|
|
150
|
+
const startAngle = -Math.PI / 2;
|
|
151
|
+
const endAngle = startAngle + scorePct * (Math.PI * 2);
|
|
152
|
+
|
|
153
|
+
ctx.beginPath();
|
|
154
|
+
ctx.arc(gaugeCenterX, gaugeCenterY, gaugeRadius, startAngle, endAngle);
|
|
155
|
+
const gaugeGrad = ctx.createLinearGradient(
|
|
156
|
+
gaugeCenterX - gaugeRadius,
|
|
157
|
+
gaugeCenterY - gaugeRadius,
|
|
158
|
+
gaugeCenterX + gaugeRadius,
|
|
159
|
+
gaugeCenterY + gaugeRadius
|
|
160
|
+
);
|
|
161
|
+
gaugeGrad.addColorStop(0, opts.primaryColor);
|
|
162
|
+
gaugeGrad.addColorStop(1, opts.secondaryColor);
|
|
163
|
+
ctx.strokeStyle = gaugeGrad;
|
|
164
|
+
ctx.lineWidth = 16;
|
|
165
|
+
ctx.stroke();
|
|
166
|
+
|
|
167
|
+
// Score Value
|
|
168
|
+
ctx.textAlign = 'center';
|
|
169
|
+
ctx.textBaseline = 'middle';
|
|
170
|
+
ctx.font = 'bold 64px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
171
|
+
ctx.fillStyle = opts.textColor;
|
|
172
|
+
ctx.fillText(`${Math.round(opts.score)}%`, gaugeCenterX, gaugeCenterY - 10);
|
|
173
|
+
|
|
174
|
+
ctx.font = '600 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
175
|
+
ctx.fillStyle = opts.mutedTextColor;
|
|
176
|
+
ctx.fillText(opts.scoreLabel, gaugeCenterX, gaugeCenterY + 36);
|
|
177
|
+
|
|
178
|
+
// Verdict Pill below gauge
|
|
179
|
+
if (opts.verdict) {
|
|
180
|
+
const pillY = gaugeCenterY + gaugeRadius + 30;
|
|
181
|
+
ctx.font = 'bold 15px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
182
|
+
const pillText = String(opts.verdict).toUpperCase();
|
|
183
|
+
const pillW = ctx.measureText(pillText).width + 36;
|
|
184
|
+
const pillH = 34;
|
|
185
|
+
|
|
186
|
+
this._drawRoundedRect(ctx, gaugeCenterX - pillW / 2, pillY, pillW, pillH, 17);
|
|
187
|
+
ctx.fillStyle = 'rgba(16, 185, 129, 0.15)';
|
|
188
|
+
ctx.fill();
|
|
189
|
+
ctx.strokeStyle = opts.verdictColor;
|
|
190
|
+
ctx.lineWidth = 1.5;
|
|
191
|
+
ctx.stroke();
|
|
192
|
+
|
|
193
|
+
ctx.fillStyle = opts.verdictColor;
|
|
194
|
+
ctx.fillText(pillText, gaugeCenterX, pillY + pillH / 2);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 7. Right Column: Headline, Subtitle & Metrics
|
|
199
|
+
ctx.textAlign = 'left';
|
|
200
|
+
ctx.textBaseline = 'top';
|
|
201
|
+
|
|
202
|
+
// Title (multi-line word wrap)
|
|
203
|
+
ctx.font = 'bold 38px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
204
|
+
ctx.fillStyle = opts.textColor;
|
|
205
|
+
const maxTitleW = cardW - (contentStartX - margin) - 40;
|
|
206
|
+
const titleLines = this._wrapText(ctx, opts.title, maxTitleW);
|
|
207
|
+
let curY = margin + 120;
|
|
208
|
+
|
|
209
|
+
titleLines.slice(0, 2).forEach((line) => {
|
|
210
|
+
ctx.fillText(line, contentStartX, curY);
|
|
211
|
+
curY += 48;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Subtitle
|
|
215
|
+
if (opts.subtitle) {
|
|
216
|
+
curY += 6;
|
|
217
|
+
ctx.font = '500 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
218
|
+
ctx.fillStyle = opts.mutedTextColor;
|
|
219
|
+
const subLines = this._wrapText(ctx, opts.subtitle, maxTitleW);
|
|
220
|
+
subLines.slice(0, 2).forEach((line) => {
|
|
221
|
+
ctx.fillText(line, contentStartX, curY);
|
|
222
|
+
curY += 26;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Metrics Breakdown Bars (if provided)
|
|
227
|
+
if (opts.showMetrics && Array.isArray(opts.metrics) && opts.metrics.length > 0) {
|
|
228
|
+
curY += 24;
|
|
229
|
+
const barTotalWidth = Math.min(maxTitleW, 640);
|
|
230
|
+
const metricsToShow = opts.metrics.slice(0, 4);
|
|
231
|
+
|
|
232
|
+
metricsToShow.forEach((metric) => {
|
|
233
|
+
const mScore = Number(metric.score ?? 0);
|
|
234
|
+
const mMax = Number(metric.maxScore ?? 10);
|
|
235
|
+
const mRatio = Math.max(0, Math.min(1, mMax > 0 ? mScore / mMax : 0));
|
|
236
|
+
|
|
237
|
+
// Metric label & score
|
|
238
|
+
ctx.font = '600 15px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
239
|
+
ctx.fillStyle = '#e2e8f0';
|
|
240
|
+
ctx.fillText(metric.label, contentStartX, curY);
|
|
241
|
+
|
|
242
|
+
ctx.textAlign = 'right';
|
|
243
|
+
ctx.fillStyle = opts.primaryColor;
|
|
244
|
+
ctx.fillText(`${mScore} / ${mMax}`, contentStartX + barTotalWidth, curY);
|
|
245
|
+
ctx.textAlign = 'left';
|
|
246
|
+
|
|
247
|
+
// Track bar
|
|
248
|
+
const barY = curY + 24;
|
|
249
|
+
const barH = 8;
|
|
250
|
+
this._drawRoundedRect(ctx, contentStartX, barY, barTotalWidth, barH, 4);
|
|
251
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
|
|
252
|
+
ctx.fill();
|
|
253
|
+
|
|
254
|
+
// Fill bar
|
|
255
|
+
if (mRatio > 0) {
|
|
256
|
+
const fillW = Math.max(8, barTotalWidth * mRatio);
|
|
257
|
+
this._drawRoundedRect(ctx, contentStartX, barY, fillW, barH, 4);
|
|
258
|
+
const barGrad = ctx.createLinearGradient(contentStartX, barY, contentStartX + fillW, barY);
|
|
259
|
+
barGrad.addColorStop(0, opts.primaryColor);
|
|
260
|
+
barGrad.addColorStop(1, opts.secondaryColor);
|
|
261
|
+
ctx.fillStyle = barGrad;
|
|
262
|
+
ctx.fill();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
curY += 46;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 8. Footer Watermark & Domain
|
|
270
|
+
const footerY = margin + cardH - 35;
|
|
271
|
+
ctx.font = '500 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
272
|
+
ctx.fillStyle = opts.mutedTextColor;
|
|
273
|
+
ctx.textAlign = 'left';
|
|
274
|
+
ctx.textBaseline = 'middle';
|
|
275
|
+
ctx.fillText(opts.watermarkText, margin + 40, footerY);
|
|
276
|
+
|
|
277
|
+
return canvas;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Generates a PNG Blob from options
|
|
282
|
+
* @param {Object} options
|
|
283
|
+
* @param {string} [type='image/png']
|
|
284
|
+
* @param {number} [quality=0.95]
|
|
285
|
+
* @returns {Promise<Blob>}
|
|
286
|
+
*/
|
|
287
|
+
static generateBlob(options = {}, type = 'image/png', quality = 0.95) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
try {
|
|
290
|
+
const canvas = document.createElement('canvas');
|
|
291
|
+
this.renderToCanvas(canvas, options);
|
|
292
|
+
canvas.toBlob((blob) => {
|
|
293
|
+
if (blob) {
|
|
294
|
+
resolve(blob);
|
|
295
|
+
} else {
|
|
296
|
+
reject(new Error('Canvas toBlob returned null'));
|
|
297
|
+
}
|
|
298
|
+
}, type, quality);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
reject(err);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generates a Data URL string from options
|
|
307
|
+
* @param {Object} options
|
|
308
|
+
* @param {string} [type='image/png']
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
311
|
+
static generateDataUrl(options = {}, type = 'image/png') {
|
|
312
|
+
const canvas = document.createElement('canvas');
|
|
313
|
+
this.renderToCanvas(canvas, options);
|
|
314
|
+
return canvas.toDataURL(type);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Internal Drawing Helpers ---
|
|
318
|
+
|
|
319
|
+
static _drawGlowOrb(ctx, x, y, radius, color, alpha) {
|
|
320
|
+
ctx.save();
|
|
321
|
+
ctx.beginPath();
|
|
322
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
323
|
+
const grad = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
324
|
+
grad.addColorStop(0, color);
|
|
325
|
+
grad.addColorStop(1, 'transparent');
|
|
326
|
+
ctx.fillStyle = grad;
|
|
327
|
+
ctx.globalAlpha = alpha;
|
|
328
|
+
ctx.fill();
|
|
329
|
+
ctx.restore();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
static _drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
333
|
+
ctx.beginPath();
|
|
334
|
+
ctx.moveTo(x + radius, y);
|
|
335
|
+
ctx.lineTo(x + width - radius, y);
|
|
336
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
337
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
338
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
339
|
+
ctx.lineTo(x + radius, y + height);
|
|
340
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
341
|
+
ctx.lineTo(x, y + radius);
|
|
342
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
343
|
+
ctx.closePath();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
static _wrapText(ctx, text, maxWidth) {
|
|
347
|
+
if (!text) return [];
|
|
348
|
+
const words = String(text).split(' ');
|
|
349
|
+
const lines = [];
|
|
350
|
+
let currentLine = words[0] || '';
|
|
351
|
+
|
|
352
|
+
for (let i = 1; i < words.length; i++) {
|
|
353
|
+
const word = words[i];
|
|
354
|
+
const width = ctx.measureText(`${currentLine} ${word}`).width;
|
|
355
|
+
if (width < maxWidth) {
|
|
356
|
+
currentLine += ` ${word}`;
|
|
357
|
+
} else {
|
|
358
|
+
lines.push(currentLine);
|
|
359
|
+
currentLine = word;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (currentLine) {
|
|
363
|
+
lines.push(currentLine);
|
|
364
|
+
}
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
367
|
+
}
|
package/Clipboard.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { swalToast, swalError } from './Alert.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Universal clipboard copy utility with automatic SweetAlert2 toast feedback.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} text - Text to copy
|
|
8
|
+
* @param {string} [successMessage='Copied to clipboard!'] - Custom toast notification
|
|
9
|
+
* @param {boolean} [showToast=true] - Whether to fire Swal toast
|
|
10
|
+
* @returns {Promise<boolean>} True if copy was successful
|
|
11
|
+
*/
|
|
12
|
+
export const copyToClipboard = async (
|
|
13
|
+
text,
|
|
14
|
+
successMessage = 'Copied to clipboard!',
|
|
15
|
+
showToast = true
|
|
16
|
+
) => {
|
|
17
|
+
if (typeof window === 'undefined') return false;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
21
|
+
await navigator.clipboard.writeText(text);
|
|
22
|
+
} else {
|
|
23
|
+
// Fallback for older browsers
|
|
24
|
+
const textarea = document.createElement('textarea');
|
|
25
|
+
textarea.value = text;
|
|
26
|
+
textarea.style.position = 'fixed';
|
|
27
|
+
textarea.style.left = '-9999px';
|
|
28
|
+
document.body.appendChild(textarea);
|
|
29
|
+
textarea.select();
|
|
30
|
+
document.execCommand('copy');
|
|
31
|
+
document.body.removeChild(textarea);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (showToast) {
|
|
35
|
+
swalToast(successMessage, 'success', 2500);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('copyToClipboard Error:', err);
|
|
40
|
+
if (showToast) {
|
|
41
|
+
swalError('Failed to copy text to clipboard.', false);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
};
|
package/Cookie.js
CHANGED
|
@@ -28,25 +28,7 @@ export const getCookie = (cname) => {
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
* Checks if a cookie with the given name exists. If not, prompts the user
|
|
33
|
-
* for a name and sets the cookie for 365 days. If the cookie does exist,
|
|
34
|
-
* alerts the user with a welcome message.
|
|
35
|
-
* @param {string} name - The name of the cookie to check.
|
|
36
|
-
*/
|
|
37
|
-
export const checkCookie = (name) => {
|
|
38
|
-
let user = getCookie(name);
|
|
39
|
-
if (!user) {
|
|
40
|
-
const askName = prompt("Please enter your name:", "");
|
|
41
|
-
if (askName) {
|
|
42
|
-
setCookie(name, askName, 365);
|
|
43
|
-
user = askName;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (user) {
|
|
47
|
-
alert(`Welcome again ${user}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
31
|
+
|
|
50
32
|
|
|
51
33
|
// CHECK TOKEN FROM COOKIE
|
|
52
34
|
export const getCookieValue = (name) => {
|
package/FormLock.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { swalToast, swalWarning } from './Alert.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry of active in-flight locks keyed by form ID or URL.
|
|
6
|
+
*/
|
|
7
|
+
const inFlightLocks = new Set();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if an operation is currently locked.
|
|
11
|
+
* @param {string} key
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export const isOperationLocked = (key) => inFlightLocks.has(key);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Acquires a lock for a key. Returns true if acquired, false if already in flight.
|
|
18
|
+
* @param {string} key
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export const acquireLock = (key) => {
|
|
22
|
+
if (inFlightLocks.has(key)) return false;
|
|
23
|
+
inFlightLocks.add(key);
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Releases a lock for a key.
|
|
29
|
+
* @param {string} key
|
|
30
|
+
*/
|
|
31
|
+
export const releaseLock = (key) => {
|
|
32
|
+
inFlightLocks.delete(key);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Guard utility to prevent concurrent or rapid double-submissions.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} lockKey - Unique identifier (e.g. formId or URL)
|
|
39
|
+
* @param {() => Promise<any>} asyncFn - Async action to run exclusively
|
|
40
|
+
* @param {string} [busyMessage='Please wait, an operation is already in progress...']
|
|
41
|
+
* @returns {Promise<any>}
|
|
42
|
+
*/
|
|
43
|
+
export const withSubmissionLock = async (
|
|
44
|
+
lockKey,
|
|
45
|
+
asyncFn,
|
|
46
|
+
busyMessage = 'Please wait, an operation is already in progress...'
|
|
47
|
+
) => {
|
|
48
|
+
if (!acquireLock(lockKey)) {
|
|
49
|
+
swalToast(busyMessage, 'warning', 2500);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return await asyncFn();
|
|
55
|
+
} finally {
|
|
56
|
+
releaseLock(lockKey);
|
|
57
|
+
}
|
|
58
|
+
};
|