@modernman00/shared-js-lib 1.2.87 → 1.2.89
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/createFormHandler.js +10 -1
- package/AcctMgt/login.js +1 -1
- package/AcctMgt/processAll.js +16 -1
- package/Alert.js +115 -1
- package/ButtonLoader.js +4 -0
- package/DigitalSignaturePad.js +545 -0
- package/FormHelper.js +14 -7
- package/README.md +7 -1
- package/config/version.php +1 -1
- package/general.js +7 -3
- package/index.js +2 -1
- package/package.json +2 -12
- package/release-history.md +14 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// createFormHandler.js
|
|
2
2
|
'use strict';
|
|
3
|
+
import { id } from '../UtilityHtml.js';
|
|
3
4
|
import { bindEvent } from '../Utility.js';
|
|
4
5
|
import { recaptchaToken } from './recaptchaToken.js';
|
|
5
6
|
import { loginSubmission } from './processAll.js';
|
|
@@ -17,7 +18,9 @@ export function createFormHandler({
|
|
|
17
18
|
recaptchaAction = 'SUBMIT'
|
|
18
19
|
}) {
|
|
19
20
|
const handler = async (e) => {
|
|
20
|
-
e.preventDefault
|
|
21
|
+
if (e && typeof e.preventDefault === 'function') {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
}
|
|
21
24
|
|
|
22
25
|
try {
|
|
23
26
|
const token = await recaptchaToken(recaptchaAction);
|
|
@@ -43,5 +46,11 @@ export function createFormHandler({
|
|
|
43
46
|
|
|
44
47
|
// 🔹 Click binding with defensive execution
|
|
45
48
|
bindEvent({ id: buttonId, handler });
|
|
49
|
+
|
|
50
|
+
// 🔹 Also bind form submit event so Enter key triggers submission
|
|
51
|
+
const formEl = id(formId);
|
|
52
|
+
if (formEl) {
|
|
53
|
+
formEl.addEventListener('submit', handler);
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
|
package/AcctMgt/login.js
CHANGED
|
@@ -42,7 +42,7 @@ export function createAdminLoginHandler({
|
|
|
42
42
|
redirect,
|
|
43
43
|
theme,
|
|
44
44
|
lengthLimit: lengthLimitArray,
|
|
45
|
-
optionalFields: [...optionalFields],
|
|
45
|
+
optionalFields: [...optionalFields, buttonId, 'button', 'loginBtn', 'submit', 'token', 'redirect'],
|
|
46
46
|
recaptchaAction
|
|
47
47
|
});
|
|
48
48
|
}
|
package/AcctMgt/processAll.js
CHANGED
|
@@ -49,13 +49,25 @@ export const loginSubmission = async ({
|
|
|
49
49
|
formData.realTimeCheckLen(lengthLimit.id, lengthLimit.max);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// On login, validate email format and non-empty password (do not enforce registration complexity regex)
|
|
53
|
+
formData.massValidate(optionalFields, { email: 'email', password: 'general' });
|
|
53
54
|
|
|
54
55
|
if (formData.result !== 1) {
|
|
55
56
|
const firstError = document.querySelector('[id$="_error"]:not(:empty)');
|
|
56
57
|
if (firstError) {
|
|
57
58
|
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
58
59
|
}
|
|
60
|
+
const errorMsg = (formData.error && formData.error.length > 0)
|
|
61
|
+
? formData.error.join('<br>')
|
|
62
|
+
: 'Please enter a valid email and password.';
|
|
63
|
+
swalError(errorMsg, true);
|
|
64
|
+
const notificationElement = id(`${formId}_notification`);
|
|
65
|
+
if (notificationElement) {
|
|
66
|
+
notificationElement.style.display = 'block';
|
|
67
|
+
notificationElement.className = 'alert alert-danger mb-3';
|
|
68
|
+
notificationElement.innerHTML = errorMsg;
|
|
69
|
+
notificationElement.scrollIntoView({ behavior: 'smooth' });
|
|
70
|
+
}
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -80,6 +92,9 @@ export const loginSubmission = async ({
|
|
|
80
92
|
// Guaranteed cleanup: remove button spinner & external loader
|
|
81
93
|
if (targetButton) {
|
|
82
94
|
clearButtonLoading(targetButton);
|
|
95
|
+
if (typeof window !== 'undefined' && window.ScoreTenantSpinner && typeof window.ScoreTenantSpinner.stop === 'function') {
|
|
96
|
+
window.ScoreTenantSpinner.stop(targetButton);
|
|
97
|
+
}
|
|
83
98
|
}
|
|
84
99
|
clearLoader('setLoader', loaderClasses);
|
|
85
100
|
}
|
package/Alert.js
CHANGED
|
@@ -139,7 +139,7 @@ export const swalConfirm = async (
|
|
|
139
139
|
try {
|
|
140
140
|
const result = await Swal.fire({
|
|
141
141
|
title,
|
|
142
|
-
text,
|
|
142
|
+
text: safeString(text),
|
|
143
143
|
icon: 'warning',
|
|
144
144
|
showCancelButton: true,
|
|
145
145
|
confirmButtonColor: '#3085d6',
|
|
@@ -155,4 +155,118 @@ export const swalConfirm = async (
|
|
|
155
155
|
}
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Defensive string converter that prevents [object Object] rendering
|
|
160
|
+
* and neutralizes XSS risks by preparing plain text for Swal.
|
|
161
|
+
*/
|
|
162
|
+
export const safeString = (val, fallback = 'Notification') => {
|
|
163
|
+
if (val === null || val === undefined) return fallback;
|
|
164
|
+
if (typeof val === 'string') {
|
|
165
|
+
const trimmed = val.trim();
|
|
166
|
+
return trimmed && trimmed !== '[object Object]' ? trimmed : fallback;
|
|
167
|
+
}
|
|
168
|
+
if (typeof val === 'object') {
|
|
169
|
+
if (typeof val.message === 'string' && val.message.trim()) return val.message.trim();
|
|
170
|
+
if (typeof val.error === 'string' && val.error.trim()) return val.error.trim();
|
|
171
|
+
if (val.response && val.response.data) {
|
|
172
|
+
const data = val.response.data;
|
|
173
|
+
if (typeof data === 'string' && data.trim()) return data.trim();
|
|
174
|
+
if (typeof data.message === 'string' && data.message.trim()) return data.message.trim();
|
|
175
|
+
if (typeof data.error === 'string' && data.error.trim()) return data.error.trim();
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const str = JSON.stringify(val);
|
|
179
|
+
if (str && str !== '{}' && str.length < 300) return str;
|
|
180
|
+
} catch (_) {}
|
|
181
|
+
}
|
|
182
|
+
return String(val) || fallback;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Universal polymorphic alert dispatcher.
|
|
187
|
+
*
|
|
188
|
+
* @param {string|Error|object} message - Message or Error object
|
|
189
|
+
* @param {'info'|'success'|'error'|'warning'|'question'} [type='info'] - Alert type
|
|
190
|
+
* @param {string|null} [title=null] - Optional title
|
|
191
|
+
* @param {boolean} [asToast=false] - Show as toast or prominent modal
|
|
192
|
+
* @returns {Promise<any>}
|
|
193
|
+
*/
|
|
194
|
+
export const showAlert = async (message, type = 'info', title = null, asToast = false) => {
|
|
195
|
+
if (!isBrowser()) return null;
|
|
196
|
+
const cleanMessage = safeString(message);
|
|
197
|
+
|
|
198
|
+
if (type === 'success') {
|
|
199
|
+
return await swalSuccess(cleanMessage, asToast, title || 'Success');
|
|
200
|
+
}
|
|
201
|
+
if (type === 'error') {
|
|
202
|
+
return await swalError(cleanMessage, !asToast, title || 'Error');
|
|
203
|
+
}
|
|
204
|
+
if (type === 'warning') {
|
|
205
|
+
return await swalWarning(cleanMessage, title || 'Warning');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (asToast) {
|
|
209
|
+
return await swalToast(cleanMessage, type);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return await Swal.fire({
|
|
213
|
+
icon: type,
|
|
214
|
+
title: title || (type.charAt(0).toUpperCase() + type.slice(1)),
|
|
215
|
+
text: cleanMessage,
|
|
216
|
+
confirmButtonColor: '#1d4ed8',
|
|
217
|
+
confirmButtonText: 'OK'
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Automatically bind SweetAlert2 globally to window.Swal and window.swal,
|
|
223
|
+
* and seamlessly intercept native window.alert so that any legacy, inline,
|
|
224
|
+
* or unbundled alert calls in any portfolio app are cleanly upgraded to
|
|
225
|
+
* modern, accessible SweetAlert2 dialogs without blocking the JavaScript thread.
|
|
226
|
+
*/
|
|
227
|
+
export const initGlobalSwalAlerts = (options = {}) => {
|
|
228
|
+
if (!isBrowser()) return;
|
|
229
|
+
|
|
230
|
+
// 1. Universal window binding
|
|
231
|
+
window.Swal = Swal;
|
|
232
|
+
window.swal = Swal;
|
|
233
|
+
window.swalToast = swalToast;
|
|
234
|
+
window.swalSuccess = swalSuccess;
|
|
235
|
+
window.swalError = swalError;
|
|
236
|
+
window.swalWarning = swalWarning;
|
|
237
|
+
window.swalConfirm = swalConfirm;
|
|
238
|
+
window.showAlert = showAlert;
|
|
239
|
+
|
|
240
|
+
// 2. Intercept native window.alert
|
|
241
|
+
if (options.interceptNativeAlert !== false && !window._swalAlertIntercepted) {
|
|
242
|
+
const originalAlert = window.alert;
|
|
243
|
+
window._nativeAlert = originalAlert;
|
|
244
|
+
window._swalAlertIntercepted = true;
|
|
245
|
+
|
|
246
|
+
window.alert = function (message) {
|
|
247
|
+
try {
|
|
248
|
+
const text = safeString(message);
|
|
249
|
+
return Swal.fire({
|
|
250
|
+
text,
|
|
251
|
+
icon: 'info',
|
|
252
|
+
confirmButtonColor: '#1d4ed8',
|
|
253
|
+
confirmButtonText: 'OK'
|
|
254
|
+
});
|
|
255
|
+
} catch (e) {
|
|
256
|
+
if (typeof originalAlert === 'function') {
|
|
257
|
+
originalAlert(message);
|
|
258
|
+
} else {
|
|
259
|
+
console.warn('SweetAlert fallback error:', e);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Automatic initialization upon module evaluation in browser environments
|
|
267
|
+
if (isBrowser()) {
|
|
268
|
+
initGlobalSwalAlerts();
|
|
269
|
+
}
|
|
270
|
+
|
|
158
271
|
export { Swal };
|
|
272
|
+
|
package/ButtonLoader.js
CHANGED
|
@@ -92,6 +92,10 @@ export const clearButtonLoading = (btn) => {
|
|
|
92
92
|
const el = resolveButton(btn);
|
|
93
93
|
if (!el) return false;
|
|
94
94
|
|
|
95
|
+
if (typeof window !== 'undefined' && window.ScoreTenantSpinner && typeof window.ScoreTenantSpinner.stop === 'function') {
|
|
96
|
+
window.ScoreTenantSpinner.stop(el);
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
if (el.hasAttribute('data-mmlib-loading')) {
|
|
96
100
|
const originalHtml = el.getAttribute('data-original-html');
|
|
97
101
|
const wasDisabledBefore = el.getAttribute('data-original-disabled') === 'true';
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigitalSignaturePad.js
|
|
3
|
+
*
|
|
4
|
+
* Enterprise Responsive HTML5 Canvas E-Signature Pad with:
|
|
5
|
+
* - Touch, Stylus & Mouse Pointer Events support
|
|
6
|
+
* - High-DPI / Retina display crisp scaling (devicePixelRatio compensation)
|
|
7
|
+
* - Bezier curve & midpoint quadratic stroke smoothing
|
|
8
|
+
* - Touch-action isolation (prevents accidental scrolling on mobile)
|
|
9
|
+
* - Dynamic baseline guide & placeholder
|
|
10
|
+
* - Clean Base64 PNG, SVG, and Blob export (isolated from UI guide markers)
|
|
11
|
+
* - Form binding & minimum-stroke validation (accidental tap rejection)
|
|
12
|
+
* - Biometric coordinate & velocity telemetry capture
|
|
13
|
+
*
|
|
14
|
+
* Compliant with eIDAS AES & UK Electronic Communications Act 2000.
|
|
15
|
+
*/
|
|
16
|
+
export class DigitalSignaturePad {
|
|
17
|
+
/**
|
|
18
|
+
* @param {HTMLCanvasElement|string} canvas - Canvas element or selector
|
|
19
|
+
* @param {Object} [options={}] - Configuration options
|
|
20
|
+
*/
|
|
21
|
+
constructor(canvas, options = {}) {
|
|
22
|
+
this.canvas = typeof canvas === 'string' ? document.querySelector(canvas) : canvas;
|
|
23
|
+
if (!this.canvas || !(this.canvas instanceof HTMLElement)) {
|
|
24
|
+
throw new Error(`[DigitalSignaturePad] Canvas element not found: ${canvas}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.ctx = this.canvas.getContext('2d');
|
|
28
|
+
if (!this.ctx) {
|
|
29
|
+
throw new Error('[DigitalSignaturePad] Unable to get 2D rendering context from canvas.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Configuration options
|
|
33
|
+
this.options = {
|
|
34
|
+
strokeColor: options.strokeColor || '#0f172a',
|
|
35
|
+
strokeWidth: options.strokeWidth || 2.5,
|
|
36
|
+
minStrokeWidth: options.minStrokeWidth || 1.5,
|
|
37
|
+
maxStrokeWidth: options.maxStrokeWidth || 3.5,
|
|
38
|
+
velocityFilterWeight: options.velocityFilterWeight || 0.7,
|
|
39
|
+
backgroundColor: options.backgroundColor || 'rgba(0, 0, 0, 0)',
|
|
40
|
+
guide: options.guide !== false, // default true
|
|
41
|
+
guideText: options.guideText !== undefined ? options.guideText : 'Sign above the line ✍️',
|
|
42
|
+
guideColor: options.guideColor || '#94a3b8',
|
|
43
|
+
minPoints: options.minPoints || 8, // Minimum point samples required for valid signature
|
|
44
|
+
onBegin: typeof options.onBegin === 'function' ? options.onBegin : null,
|
|
45
|
+
onEnd: typeof options.onEnd === 'function' ? options.onEnd : null,
|
|
46
|
+
onChange: typeof options.onChange === 'function' ? options.onChange : null,
|
|
47
|
+
readOnly: !!options.readOnly
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// State tracking
|
|
51
|
+
this.strokes = []; // Array of strokes, each stroke is an array of point objects
|
|
52
|
+
this.currentStroke = null;
|
|
53
|
+
this.isDrawing = false;
|
|
54
|
+
this.dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
|
|
55
|
+
|
|
56
|
+
// Apply mobile touch isolation
|
|
57
|
+
this.canvas.style.touchAction = 'none';
|
|
58
|
+
|
|
59
|
+
// Bound listeners for clean destruction
|
|
60
|
+
this._handlePointerDown = this._onPointerDown.bind(this);
|
|
61
|
+
this._handlePointerMove = this._onPointerMove.bind(this);
|
|
62
|
+
this._handlePointerUp = this._onPointerUp.bind(this);
|
|
63
|
+
this._handleResize = this._onResize.bind(this);
|
|
64
|
+
|
|
65
|
+
this._initCanvas();
|
|
66
|
+
this._attachEvents();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initializes high-DPI scaling and baseline guide.
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
_initCanvas() {
|
|
74
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
75
|
+
const width = rect.width || this.canvas.width || 400;
|
|
76
|
+
const height = rect.height || this.canvas.height || 200;
|
|
77
|
+
|
|
78
|
+
this.width = width;
|
|
79
|
+
this.height = height;
|
|
80
|
+
|
|
81
|
+
this.canvas.width = Math.round(width * this.dpr);
|
|
82
|
+
this.canvas.height = Math.round(height * this.dpr);
|
|
83
|
+
|
|
84
|
+
if (this.ctx) {
|
|
85
|
+
this.ctx.scale(this.dpr, this.dpr);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._redraw();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Attaches pointer and window events.
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_attachEvents() {
|
|
96
|
+
if (this.options.readOnly) return;
|
|
97
|
+
|
|
98
|
+
this.canvas.addEventListener('pointerdown', this._handlePointerDown, { passive: false });
|
|
99
|
+
this.canvas.addEventListener('pointermove', this._handlePointerMove, { passive: false });
|
|
100
|
+
this.canvas.addEventListener('pointerup', this._handlePointerUp);
|
|
101
|
+
this.canvas.addEventListener('pointercancel', this._handlePointerUp);
|
|
102
|
+
this.canvas.addEventListener('pointerleave', this._handlePointerUp);
|
|
103
|
+
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
window.addEventListener('resize', this._handleResize);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detaches event listeners.
|
|
111
|
+
*/
|
|
112
|
+
destroy() {
|
|
113
|
+
this.canvas.removeEventListener('pointerdown', this._handlePointerDown);
|
|
114
|
+
this.canvas.removeEventListener('pointermove', this._handlePointerMove);
|
|
115
|
+
this.canvas.removeEventListener('pointerup', this._handlePointerUp);
|
|
116
|
+
this.canvas.removeEventListener('pointercancel', this._handlePointerUp);
|
|
117
|
+
this.canvas.removeEventListener('pointerleave', this._handlePointerUp);
|
|
118
|
+
|
|
119
|
+
if (typeof window !== 'undefined') {
|
|
120
|
+
window.removeEventListener('resize', this._handleResize);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handles window resize with high-DPI preservation.
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
_onResize() {
|
|
129
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
130
|
+
if (!rect.width || !rect.height) return;
|
|
131
|
+
|
|
132
|
+
this.dpr = (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1;
|
|
133
|
+
this.width = rect.width;
|
|
134
|
+
this.height = rect.height;
|
|
135
|
+
|
|
136
|
+
this.canvas.width = Math.round(this.width * this.dpr);
|
|
137
|
+
this.canvas.height = Math.round(this.height * this.dpr);
|
|
138
|
+
|
|
139
|
+
if (this.ctx) {
|
|
140
|
+
this.ctx.scale(this.dpr, this.dpr);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this._redraw();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Normalizes client coordinates into canvas coordinate space.
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
_getPoint(event) {
|
|
151
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
152
|
+
const x = event.clientX - rect.left;
|
|
153
|
+
const y = event.clientY - rect.top;
|
|
154
|
+
const time = Date.now();
|
|
155
|
+
const pressure = event.pressure !== undefined && event.pressure > 0 ? event.pressure : 0.5;
|
|
156
|
+
|
|
157
|
+
return { x, y, time, pressure };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Pointer down handler.
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
_onPointerDown(event) {
|
|
165
|
+
if (this.options.readOnly || event.button !== 0) return;
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
|
|
168
|
+
this.isDrawing = true;
|
|
169
|
+
const point = this._getPoint(event);
|
|
170
|
+
this.currentStroke = [point];
|
|
171
|
+
this.strokes.push(this.currentStroke);
|
|
172
|
+
|
|
173
|
+
if (this.options.onBegin) {
|
|
174
|
+
this.options.onBegin(point);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this._redraw();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Pointer move handler with midpoint curve smoothing.
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_onPointerMove(event) {
|
|
185
|
+
if (!this.isDrawing || !this.currentStroke) return;
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
|
|
188
|
+
const point = this._getPoint(event);
|
|
189
|
+
const stroke = this.currentStroke;
|
|
190
|
+
const prevPoint = stroke[stroke.length - 1];
|
|
191
|
+
|
|
192
|
+
// Ignore jitter/micro-movement (< 1.5px)
|
|
193
|
+
const dist = Math.hypot(point.x - prevPoint.x, point.y - prevPoint.y);
|
|
194
|
+
if (dist < 1.5) return;
|
|
195
|
+
|
|
196
|
+
stroke.push(point);
|
|
197
|
+
|
|
198
|
+
this._drawLastSegment(stroke);
|
|
199
|
+
|
|
200
|
+
if (this.options.onChange) {
|
|
201
|
+
this.options.onChange();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pointer up / release handler.
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_onPointerUp(event) {
|
|
210
|
+
if (!this.isDrawing) return;
|
|
211
|
+
this.isDrawing = false;
|
|
212
|
+
this.currentStroke = null;
|
|
213
|
+
|
|
214
|
+
if (this.options.onEnd) {
|
|
215
|
+
this.options.onEnd();
|
|
216
|
+
}
|
|
217
|
+
if (this.options.onChange) {
|
|
218
|
+
this.options.onChange();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Draws the latest curve segment for fluid 60fps rendering without redrawing the whole canvas.
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
_drawLastSegment(stroke) {
|
|
227
|
+
if (!this.ctx || stroke.length < 2) return;
|
|
228
|
+
|
|
229
|
+
const ctx = this.ctx;
|
|
230
|
+
const p1 = stroke[stroke.length - 2];
|
|
231
|
+
const p2 = stroke[stroke.length - 1];
|
|
232
|
+
|
|
233
|
+
ctx.save();
|
|
234
|
+
ctx.strokeStyle = this.options.strokeColor;
|
|
235
|
+
ctx.lineCap = 'round';
|
|
236
|
+
ctx.lineJoin = 'round';
|
|
237
|
+
|
|
238
|
+
// Calculate dynamic stroke width based on distance & velocity
|
|
239
|
+
const width = this._calculateStrokeWidth(p1, p2);
|
|
240
|
+
ctx.lineWidth = width;
|
|
241
|
+
|
|
242
|
+
ctx.beginPath();
|
|
243
|
+
if (stroke.length === 2) {
|
|
244
|
+
ctx.moveTo(p1.x, p1.y);
|
|
245
|
+
ctx.lineTo(p2.x, p2.y);
|
|
246
|
+
} else {
|
|
247
|
+
const p0 = stroke[stroke.length - 3];
|
|
248
|
+
const mid1 = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
|
|
249
|
+
const mid2 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
|
|
250
|
+
ctx.moveTo(mid1.x, mid1.y);
|
|
251
|
+
ctx.quadraticCurveTo(p1.x, p1.y, mid2.x, mid2.y);
|
|
252
|
+
}
|
|
253
|
+
ctx.stroke();
|
|
254
|
+
ctx.restore();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Calculates velocity-based dynamic stroke width.
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
_calculateStrokeWidth(p1, p2) {
|
|
262
|
+
const timeDelta = Math.max(p2.time - p1.time, 1);
|
|
263
|
+
const distance = Math.hypot(p2.x - p1.x, p2.y - p1.y);
|
|
264
|
+
const velocity = distance / timeDelta;
|
|
265
|
+
|
|
266
|
+
const { minStrokeWidth, maxStrokeWidth, strokeWidth } = this.options;
|
|
267
|
+
// Faster velocity -> thinner line (fountain pen stroke dynamics)
|
|
268
|
+
const dynamicWidth = Math.max(
|
|
269
|
+
minStrokeWidth,
|
|
270
|
+
Math.min(maxStrokeWidth, strokeWidth / (velocity * 0.5 + 1))
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return dynamicWidth;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Full canvas redraw: background, guide (if empty), and all strokes.
|
|
278
|
+
* @param {boolean} [includeGuide=true] - Whether to draw the baseline guide
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
_redraw(includeGuide = true) {
|
|
282
|
+
if (!this.ctx) return;
|
|
283
|
+
const ctx = this.ctx;
|
|
284
|
+
|
|
285
|
+
ctx.save();
|
|
286
|
+
ctx.clearRect(0, 0, this.width, this.height);
|
|
287
|
+
|
|
288
|
+
// 1. Draw background
|
|
289
|
+
if (this.options.backgroundColor && this.options.backgroundColor !== 'rgba(0, 0, 0, 0)') {
|
|
290
|
+
ctx.fillStyle = this.options.backgroundColor;
|
|
291
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 2. Draw placeholder guide if empty and enabled
|
|
295
|
+
if (includeGuide && this.options.guide && this.isEmpty()) {
|
|
296
|
+
this._drawGuide();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 3. Draw all recorded strokes with midpoint quadratic curves
|
|
300
|
+
for (const stroke of this.strokes) {
|
|
301
|
+
if (stroke.length === 0) continue;
|
|
302
|
+
|
|
303
|
+
ctx.save();
|
|
304
|
+
ctx.strokeStyle = this.options.strokeColor;
|
|
305
|
+
ctx.lineCap = 'round';
|
|
306
|
+
ctx.lineJoin = 'round';
|
|
307
|
+
|
|
308
|
+
if (stroke.length === 1) {
|
|
309
|
+
// Single tap dot
|
|
310
|
+
ctx.fillStyle = this.options.strokeColor;
|
|
311
|
+
ctx.beginPath();
|
|
312
|
+
ctx.arc(stroke[0].x, stroke[0].y, this.options.strokeWidth / 2, 0, Math.PI * 2);
|
|
313
|
+
ctx.fill();
|
|
314
|
+
} else {
|
|
315
|
+
for (let i = 1; i < stroke.length; i++) {
|
|
316
|
+
const p1 = stroke[i - 1];
|
|
317
|
+
const p2 = stroke[i];
|
|
318
|
+
ctx.lineWidth = this._calculateStrokeWidth(p1, p2);
|
|
319
|
+
|
|
320
|
+
ctx.beginPath();
|
|
321
|
+
if (i === 1) {
|
|
322
|
+
ctx.moveTo(p1.x, p1.y);
|
|
323
|
+
ctx.lineTo(p2.x, p2.y);
|
|
324
|
+
} else {
|
|
325
|
+
const p0 = stroke[i - 2];
|
|
326
|
+
const mid1 = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
|
|
327
|
+
const mid2 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
|
|
328
|
+
ctx.moveTo(mid1.x, mid1.y);
|
|
329
|
+
ctx.quadraticCurveTo(p1.x, p1.y, mid2.x, mid2.y);
|
|
330
|
+
}
|
|
331
|
+
ctx.stroke();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
ctx.restore();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
ctx.restore();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Draws a subtle dashed baseline and prompt text for guidance.
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
_drawGuide() {
|
|
345
|
+
if (!this.ctx) return;
|
|
346
|
+
const ctx = this.ctx;
|
|
347
|
+
const guideY = Math.round(this.height * 0.75);
|
|
348
|
+
|
|
349
|
+
ctx.save();
|
|
350
|
+
ctx.strokeStyle = this.options.guideColor;
|
|
351
|
+
ctx.lineWidth = 1;
|
|
352
|
+
if (typeof ctx.setLineDash === 'function') {
|
|
353
|
+
ctx.setLineDash([4, 4]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Baseline
|
|
357
|
+
ctx.beginPath();
|
|
358
|
+
ctx.moveTo(20, guideY);
|
|
359
|
+
ctx.lineTo(this.width - 20, guideY);
|
|
360
|
+
ctx.stroke();
|
|
361
|
+
|
|
362
|
+
// Helper text
|
|
363
|
+
if (this.options.guideText) {
|
|
364
|
+
ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
365
|
+
ctx.fillStyle = this.options.guideColor;
|
|
366
|
+
ctx.textAlign = 'center';
|
|
367
|
+
ctx.fillText(this.options.guideText, this.width / 2, guideY + 18);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
ctx.restore();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Returns true if no valid strokes have been drawn or if points are under the minimum threshold.
|
|
375
|
+
* Rejects accidental 1-pixel taps to ensure genuine biometric intent.
|
|
376
|
+
*
|
|
377
|
+
* @returns {boolean}
|
|
378
|
+
*/
|
|
379
|
+
isEmpty() {
|
|
380
|
+
let totalPoints = 0;
|
|
381
|
+
for (const stroke of this.strokes) {
|
|
382
|
+
totalPoints += stroke.length;
|
|
383
|
+
}
|
|
384
|
+
return totalPoints < this.options.minPoints;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Boolean check whether the signature satisfies minimum valid biometric threshold.
|
|
389
|
+
* @returns {boolean}
|
|
390
|
+
*/
|
|
391
|
+
isValid() {
|
|
392
|
+
return !this.isEmpty();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Clears all strokes, resets canvas, and restores the guide placeholder.
|
|
397
|
+
*/
|
|
398
|
+
clear() {
|
|
399
|
+
this.strokes = [];
|
|
400
|
+
this.currentStroke = null;
|
|
401
|
+
this.isDrawing = false;
|
|
402
|
+
this._redraw(true);
|
|
403
|
+
|
|
404
|
+
if (this.options.onChange) {
|
|
405
|
+
this.options.onChange();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Exports signature as Base64 Data URL (PNG by default).
|
|
411
|
+
* Guaranteed to isolate the pure signature from UI guide placeholders.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} [type='image/png']
|
|
414
|
+
* @param {number} [quality=0.92]
|
|
415
|
+
* @returns {string}
|
|
416
|
+
*/
|
|
417
|
+
toDataURL(type = 'image/png', quality = 0.92) {
|
|
418
|
+
if (this.isEmpty()) {
|
|
419
|
+
return '';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Redraw without guide line for clean legal artifact export
|
|
423
|
+
this._redraw(false);
|
|
424
|
+
const dataUrl = this.canvas.toDataURL(type, quality);
|
|
425
|
+
// Restore guide line if needed
|
|
426
|
+
this._redraw(true);
|
|
427
|
+
|
|
428
|
+
return dataUrl;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Exports signature as a Blob.
|
|
433
|
+
*
|
|
434
|
+
* @param {Function} callback - (blob) => void
|
|
435
|
+
* @param {string} [type='image/png']
|
|
436
|
+
* @param {number} [quality=0.92]
|
|
437
|
+
*/
|
|
438
|
+
toBlob(callback, type = 'image/png', quality = 0.92) {
|
|
439
|
+
if (this.isEmpty()) {
|
|
440
|
+
callback(null);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this._redraw(false);
|
|
445
|
+
this.canvas.toBlob((blob) => {
|
|
446
|
+
this._redraw(true);
|
|
447
|
+
callback(blob);
|
|
448
|
+
}, type, quality);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Exports strokes as an SVG vector string.
|
|
453
|
+
* Ideal for high-resolution print agreements and PDF rendering.
|
|
454
|
+
*
|
|
455
|
+
* @returns {string}
|
|
456
|
+
*/
|
|
457
|
+
toSVG() {
|
|
458
|
+
if (this.isEmpty()) {
|
|
459
|
+
return '';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let paths = '';
|
|
463
|
+
for (const stroke of this.strokes) {
|
|
464
|
+
if (stroke.length === 0) continue;
|
|
465
|
+
let d = `M ${stroke[0].x.toFixed(1)} ${stroke[0].y.toFixed(1)}`;
|
|
466
|
+
for (let i = 1; i < stroke.length; i++) {
|
|
467
|
+
const p1 = stroke[i - 1];
|
|
468
|
+
const p2 = stroke[i];
|
|
469
|
+
const midX = ((p1.x + p2.x) / 2).toFixed(1);
|
|
470
|
+
const midY = ((p1.y + p2.y) / 2).toFixed(1);
|
|
471
|
+
d += ` Q ${p1.x.toFixed(1)} ${p1.y.toFixed(1)} ${midX} ${midY}`;
|
|
472
|
+
}
|
|
473
|
+
paths += `<path d="${d}" fill="none" stroke="${this.options.strokeColor}" stroke-width="${this.options.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" />\n`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${this.width} ${this.height}" width="${this.width}" height="${this.height}">\n${paths}</svg>`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Returns biometric telemetry coordinates for audit records.
|
|
481
|
+
* @returns {Array<Array<{x: number, y: number, time: number, pressure: number}>>}
|
|
482
|
+
*/
|
|
483
|
+
getStrokes() {
|
|
484
|
+
return JSON.parse(JSON.stringify(this.strokes));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Restores strokes from serialized biometric coordinates.
|
|
489
|
+
* @param {Array<Array<{x: number, y: number, time: number, pressure: number}>>} strokes
|
|
490
|
+
*/
|
|
491
|
+
fromStrokes(strokes) {
|
|
492
|
+
if (!Array.isArray(strokes)) return;
|
|
493
|
+
this.strokes = JSON.parse(JSON.stringify(strokes));
|
|
494
|
+
this._redraw(true);
|
|
495
|
+
if (this.options.onChange) {
|
|
496
|
+
this.options.onChange();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Seamlessly binds the signature pad to an HTML form.
|
|
502
|
+
* On form submit:
|
|
503
|
+
* - Validates that a genuine signature is present (!isEmpty()).
|
|
504
|
+
* - Injects the Base64 signature into a hidden input.
|
|
505
|
+
* - Blocks submit and invokes onValidationError callback if empty.
|
|
506
|
+
*
|
|
507
|
+
* @param {HTMLFormElement|string} form - Form element or selector
|
|
508
|
+
* @param {Object} [bindOptions={}]
|
|
509
|
+
* @param {string} [bindOptions.inputName='signature_base64'] - Name/id of hidden input
|
|
510
|
+
* @param {Function} [bindOptions.onValidationError] - Invoked when user attempts submit without signing
|
|
511
|
+
* @returns {HTMLInputElement} The bound hidden input
|
|
512
|
+
*/
|
|
513
|
+
bindToForm(form, bindOptions = {}) {
|
|
514
|
+
const formEl = typeof form === 'string' ? document.querySelector(form) : form;
|
|
515
|
+
if (!formEl || !(formEl instanceof HTMLFormElement)) {
|
|
516
|
+
throw new Error(`[DigitalSignaturePad] Target form element not found: ${form}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const inputName = bindOptions.inputName || 'signature_base64';
|
|
520
|
+
let hiddenInput = formEl.querySelector(`input[name="${inputName}"]`);
|
|
521
|
+
if (!hiddenInput) {
|
|
522
|
+
hiddenInput = document.createElement('input');
|
|
523
|
+
hiddenInput.type = 'hidden';
|
|
524
|
+
hiddenInput.name = inputName;
|
|
525
|
+
formEl.appendChild(hiddenInput);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
formEl.addEventListener('submit', (e) => {
|
|
529
|
+
if (this.isEmpty()) {
|
|
530
|
+
e.preventDefault();
|
|
531
|
+
if (typeof bindOptions.onValidationError === 'function') {
|
|
532
|
+
bindOptions.onValidationError('Please sign the agreement above before proceeding.');
|
|
533
|
+
} else {
|
|
534
|
+
alert('Please provide your signature before submitting.');
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
hiddenInput.value = this.toDataURL();
|
|
540
|
+
return true;
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return hiddenInput;
|
|
544
|
+
}
|
|
545
|
+
}
|
package/FormHelper.js
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
export default class FormHelper {
|
|
3
3
|
constructor(data) {
|
|
4
4
|
if (!Array.isArray(data)) throw new Error('data must be an array of form elements');
|
|
5
|
-
this.data = data
|
|
5
|
+
this.data = data.filter(el => {
|
|
6
|
+
if (!el || !el.tagName) return false;
|
|
7
|
+
const tag = el.tagName.toUpperCase();
|
|
8
|
+
if (tag === 'BUTTON') return false;
|
|
9
|
+
const type = (el.type || '').toLowerCase();
|
|
10
|
+
if (['submit', 'reset', 'button', 'hidden'].includes(type)) return false;
|
|
11
|
+
return true;
|
|
12
|
+
});
|
|
6
13
|
this.error = [];
|
|
7
14
|
this.result = 0;
|
|
8
15
|
}
|
|
@@ -114,7 +121,9 @@ export default class FormHelper {
|
|
|
114
121
|
!id ||
|
|
115
122
|
['submit', 'button', 'g-recaptcha-response', 'cancel'].includes(name) ||
|
|
116
123
|
['button', 'showPassword', 'token', 'g-recaptcha-response'].includes(id) ||
|
|
117
|
-
|
|
124
|
+
['button', 'submit', 'reset', 'hidden'].includes(type) ||
|
|
125
|
+
(input.tagName && input.tagName.toUpperCase() === 'BUTTON') ||
|
|
126
|
+
optionalFields.includes(id)
|
|
118
127
|
) continue;
|
|
119
128
|
|
|
120
129
|
// Skip hidden elements (if the user can't see it, they can't fill it)
|
|
@@ -123,10 +132,7 @@ export default class FormHelper {
|
|
|
123
132
|
continue;
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const errorEl = this.id(`${id}_error`);
|
|
135
|
+
const errorEl = this.id(`${id}_error`);
|
|
130
136
|
let val = value.trim();
|
|
131
137
|
|
|
132
138
|
// Handle optional fields - If optional but empty → auto‑fill “Not Provided”
|
|
@@ -136,7 +142,8 @@ export default class FormHelper {
|
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
// Required field check
|
|
139
|
-
|
|
145
|
+
const isUncheckedRequiredCheckbox = type === 'checkbox' && !input.checked && (input.required || !optionalFields.includes(id));
|
|
146
|
+
if (val === '' || val === 'select' || isUncheckedRequiredCheckbox) {
|
|
140
147
|
if (errorEl) errorEl.innerHTML = `<li style="color:red;">${id} cannot be left empty.</li>`;
|
|
141
148
|
this.error.push(`${id.toUpperCase()} cannot be left empty.`);
|
|
142
149
|
continue;
|
package/README.md
CHANGED
|
@@ -34,7 +34,10 @@ console.log(cleanInput); // Sanitized output
|
|
|
34
34
|
|
|
35
35
|
### Example: SweetAlert2 Notifications & Modals
|
|
36
36
|
```javascript
|
|
37
|
-
import { swalToast, swalSuccess, swalError, swalConfirm } from '@modernman00/shared-js-lib';
|
|
37
|
+
import { swalToast, swalSuccess, swalError, swalConfirm, showAlert, initGlobalSwalAlerts } from '@modernman00/shared-js-lib';
|
|
38
|
+
|
|
39
|
+
// Auto-initialize global SweetAlert bindings and intercept native window.alert()
|
|
40
|
+
initGlobalSwalAlerts();
|
|
38
41
|
|
|
39
42
|
// Fast non-blocking toast at top-end
|
|
40
43
|
await swalToast('Profile updated successfully!');
|
|
@@ -43,6 +46,9 @@ await swalToast('Profile updated successfully!');
|
|
|
43
46
|
await swalSuccess('Your application has been submitted.', false);
|
|
44
47
|
await swalError('Payment processing failed. Please check your card.');
|
|
45
48
|
|
|
49
|
+
// Universal polymorphic alert
|
|
50
|
+
await showAlert('System update scheduled at midnight', 'info');
|
|
51
|
+
|
|
46
52
|
// Interactive confirmation dialog
|
|
47
53
|
const proceed = await swalConfirm('Delete Account', 'Are you sure? This cannot be undone.');
|
|
48
54
|
if (proceed) {
|
package/config/version.php
CHANGED
package/general.js
CHANGED
|
@@ -52,9 +52,13 @@ export const parseErrorResponse = (error) => {
|
|
|
52
52
|
|
|
53
53
|
if (Array.isArray(error?.response?.data?.message)) {
|
|
54
54
|
errorMessage = `<ul>${error.response.data.message.map(e => `<li>${e}</li>`).join('')}</ul>`;
|
|
55
|
-
} else if (typeof error?.response?.data?.message === 'string') {
|
|
56
|
-
errorMessage = error.response.data.message;
|
|
57
|
-
} else if (typeof error
|
|
55
|
+
} else if (typeof error?.response?.data?.message === 'string' && error.response.data.message.trim()) {
|
|
56
|
+
errorMessage = error.response.data.message.trim();
|
|
57
|
+
} else if (typeof error?.response?.data?.error === 'string' && error.response.data.error.trim()) {
|
|
58
|
+
errorMessage = error.response.data.error.trim();
|
|
59
|
+
} else if (Array.isArray(error?.response?.data?.errors) && error.response.data.errors.length > 0) {
|
|
60
|
+
errorMessage = `<ul>${error.response.data.errors.map(e => `<li>${e}</li>`).join('')}</ul>`;
|
|
61
|
+
} else if (typeof error?.message === 'string') {
|
|
58
62
|
errorMessage = error.message;
|
|
59
63
|
}
|
|
60
64
|
|
package/index.js
CHANGED
|
@@ -31,4 +31,5 @@ export * from './CardCanvasGenerator.js';
|
|
|
31
31
|
export * from './ShareManager.js';
|
|
32
32
|
export * from './WebAuthnClient.js';
|
|
33
33
|
export * from './OtpInputManager.js';
|
|
34
|
-
export * from './AddressAutocomplete.js';
|
|
34
|
+
export * from './AddressAutocomplete.js';
|
|
35
|
+
export * from './DigitalSignaturePad.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.89",
|
|
4
4
|
"description": "Reusable JS utilities for numerous js problems",
|
|
5
5
|
"homepage": "https://github.com/modernman00/shared-js-lib#readme",
|
|
6
6
|
"keywords": [
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"ShareManager.js",
|
|
65
65
|
"OtpInputManager.js",
|
|
66
66
|
"AddressAutocomplete.js",
|
|
67
|
+
"DigitalSignaturePad.js",
|
|
67
68
|
"templates/"
|
|
68
69
|
],
|
|
69
70
|
"devDependencies": {
|
|
@@ -79,16 +80,5 @@
|
|
|
79
80
|
"jest": "^30.0.5",
|
|
80
81
|
"jest-environment-jsdom": "^30.0.5",
|
|
81
82
|
"semantic-release": "^25.0.2"
|
|
82
|
-
},
|
|
83
|
-
"release": {
|
|
84
|
-
"branches": [
|
|
85
|
-
"main"
|
|
86
|
-
],
|
|
87
|
-
"plugins": [
|
|
88
|
-
"@semantic-release/npm",
|
|
89
|
-
"@semantic-release/changelog",
|
|
90
|
-
"@semantic-release/git",
|
|
91
|
-
"@semantic-release/github"
|
|
92
|
-
]
|
|
93
83
|
}
|
|
94
84
|
}
|
package/release-history.md
CHANGED
|
@@ -158,3 +158,17 @@
|
|
|
158
158
|
- feat(pwa): harden PwaManager badging sync, support diverse envelopes, and dispatch pwa:badge-updated event _(by modernman00)_
|
|
159
159
|
- 🔖 Release v1.2.84 _(by modernman00)_
|
|
160
160
|
- 🧹 Auto-commit before release _(by modernman00)_
|
|
161
|
+
|
|
162
|
+
## v1.2.88 — 2026-10-14 18:00
|
|
163
|
+
### Changelog for v1.2.88
|
|
164
|
+
|
|
165
|
+
- 🧹 Auto-commit before release _(by modernman00)_
|
|
166
|
+
|
|
167
|
+
## v1.2.89 — 2026-10-23 16:54
|
|
168
|
+
### Changelog for v1.2.89
|
|
169
|
+
|
|
170
|
+
- test: compare FormHelper.getData() by value, constructor returns a filtered copy _(by modernman00)_
|
|
171
|
+
- ci: remove duplicate release workflow and unused semantic-release config _(by modernman00)_
|
|
172
|
+
- 🧹 Auto-commit before release _(by modernman00)_
|
|
173
|
+
- 🔖 Release v1.2.88 _(by modernman00)_
|
|
174
|
+
- 🧹 Auto-commit before release _(by modernman00)_
|