@modernman00/shared-js-lib 1.2.81 → 1.2.82
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/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/README.md +74 -0
- package/ShowResponse.js +120 -0
- package/axiosWrapper.js +3 -0
- package/config/version.php +1 -1
- package/index.js +8 -0
- package/package.json +10 -2
- package/release-history.md +20 -0
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
|
+
};
|
package/Http.js
CHANGED
|
@@ -1,90 +1,191 @@
|
|
|
1
1
|
import { id, log } from './UtilityHtml.js';
|
|
2
2
|
import axios from './axiosWrapper.js';
|
|
3
3
|
import { redirectAfterDelay, parseErrorResponse } from './general.js';
|
|
4
|
+
import { swalSuccess, swalError, swalToast } from './Alert.js';
|
|
5
|
+
import { setButtonLoading, clearButtonLoading } from './ButtonLoader.js';
|
|
6
|
+
import { acquireLock, releaseLock } from './FormLock.js';
|
|
7
|
+
import { isOnline } from './NetworkGuard.js';
|
|
4
8
|
|
|
5
9
|
/**
|
|
10
|
+
* Sends form data via POST request with automatic button spinner, SweetAlert2 alerts, and robust error handling.
|
|
11
|
+
*
|
|
12
|
+
* Can be called with positional arguments (backward-compatible):
|
|
13
|
+
* postFormData(url, formId, redirect, css, recaptchaToken, button)
|
|
14
|
+
*
|
|
15
|
+
* Or with a modern config object:
|
|
16
|
+
* postFormData(url, formId, { redirect, css, button, recaptchaToken, resetForm })
|
|
6
17
|
*
|
|
7
|
-
* Sends form data via POST request.
|
|
8
18
|
* @param {string} url - The URL to post the data to.
|
|
9
|
-
* @param {string} formId - The ID or
|
|
10
|
-
* @param {string|null}
|
|
11
|
-
* @param {string|null} css -
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
* @param {string|HTMLFormElement} formId - The ID string or DOM element of the form.
|
|
20
|
+
* @param {string|Object|null} [redirectOrOptions=null] - Redirect URL, or options configuration object.
|
|
21
|
+
* @param {string|null} [css=null] - CSS framework ('bootstrap', 'bulma', 'W3css', 'tailwind').
|
|
22
|
+
* @param {Object|null} [recaptchaToken=null] - reCAPTCHA token payload ({ token, action }).
|
|
23
|
+
* @param {string|HTMLElement|null} [button=null] - Specific button ID or element to show spinner on.
|
|
24
|
+
* @returns {Promise<Object>} Axios response object on success.
|
|
14
25
|
*/
|
|
15
|
-
export const postFormData = async (
|
|
26
|
+
export const postFormData = async (
|
|
27
|
+
url,
|
|
28
|
+
formId,
|
|
29
|
+
redirectOrOptions = null,
|
|
30
|
+
css = null,
|
|
31
|
+
recaptchaToken = null,
|
|
32
|
+
button = null
|
|
33
|
+
) => {
|
|
34
|
+
// Determine if third argument is an options object
|
|
35
|
+
const isOptionsObj = redirectOrOptions !== null && typeof redirectOrOptions === 'object';
|
|
36
|
+
const redirect = isOptionsObj ? redirectOrOptions.redirect : redirectOrOptions;
|
|
37
|
+
const themeCss = isOptionsObj ? (redirectOrOptions.css || css) : css;
|
|
38
|
+
const recaptcha = isOptionsObj ? (redirectOrOptions.recaptchaToken || recaptchaToken) : recaptchaToken;
|
|
39
|
+
const buttonTarget = isOptionsObj ? (redirectOrOptions.button || button) : button;
|
|
40
|
+
const shouldResetForm = isOptionsObj && typeof redirectOrOptions.resetForm === 'boolean'
|
|
41
|
+
? redirectOrOptions.resetForm
|
|
42
|
+
: true;
|
|
43
|
+
|
|
44
|
+
// 1. Connectivity Check
|
|
45
|
+
if (!isOnline()) {
|
|
46
|
+
swalToast('No internet connection. Please check your network and retry.', 'warning', 4000);
|
|
47
|
+
return { error: true, message: 'Offline' };
|
|
48
|
+
}
|
|
16
49
|
|
|
17
|
-
|
|
18
|
-
|
|
50
|
+
// 2. Double-Submit Protection Lock
|
|
51
|
+
const lockKey = `${url}::${typeof formId === 'string' ? formId : (formId.id || 'form')}`;
|
|
52
|
+
if (!acquireLock(lockKey)) {
|
|
53
|
+
swalToast('Please wait, a submission is already in progress...', 'warning', 2500);
|
|
54
|
+
return { error: true, message: 'In flight' };
|
|
55
|
+
}
|
|
19
56
|
|
|
20
|
-
|
|
21
|
-
const
|
|
57
|
+
// Resolve form element
|
|
58
|
+
const form = typeof formId === 'string' ? id(formId) : formId;
|
|
59
|
+
if (!form) {
|
|
60
|
+
releaseLock(lockKey);
|
|
61
|
+
throw new Error('Form element not found');
|
|
62
|
+
}
|
|
22
63
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
// Resolve button for spinner:
|
|
65
|
+
// Works with any button type: type="button", type="submit", or no type.
|
|
66
|
+
let btnEl = null;
|
|
67
|
+
if (buttonTarget) {
|
|
68
|
+
btnEl = typeof buttonTarget === 'string'
|
|
69
|
+
? (id(buttonTarget.replace(/^#/, '')) || document.querySelector(buttonTarget))
|
|
70
|
+
: buttonTarget;
|
|
71
|
+
}
|
|
72
|
+
if (!btnEl && form instanceof HTMLFormElement) {
|
|
73
|
+
// Smart fallback priority:
|
|
74
|
+
// 1. type="submit" or input submit
|
|
75
|
+
// 2. Any button with common action classes or IDs (save, submit)
|
|
76
|
+
// 3. ANY button in the form, including type="button"
|
|
77
|
+
btnEl = form.querySelector('button[type="submit"], input[type="submit"]')
|
|
78
|
+
|| form.querySelector('button#submit, button#save, button.btn-primary, button.is-primary')
|
|
79
|
+
|| form.querySelector('button');
|
|
26
80
|
}
|
|
27
|
-
// Cleanup previous notification styles
|
|
28
|
-
notificationId.style.display = 'none';
|
|
29
81
|
|
|
30
|
-
|
|
82
|
+
// Set button spinner and disable button
|
|
83
|
+
if (btnEl) {
|
|
84
|
+
setButtonLoading(btnEl);
|
|
85
|
+
}
|
|
31
86
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// const csrfInput = id('token');
|
|
36
|
-
// if (csrfInput) {
|
|
37
|
-
// csrfInput.value = csrfToken; // Set the CSRF token in the hidden input field
|
|
38
|
-
// }
|
|
87
|
+
// Optional legacy external loader
|
|
88
|
+
const loader = id('setLoader');
|
|
89
|
+
if (loader) loader.style.display = 'block';
|
|
39
90
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
91
|
+
// Determine notification requirement:
|
|
92
|
+
// If called with modern options object, notification is optional (SweetAlert2 handles alerts).
|
|
93
|
+
// If called with legacy positional arguments, enforce legacy expectation if configured.
|
|
94
|
+
const formIdStr = typeof formId === 'string' ? formId : (form.id || '');
|
|
95
|
+
const notificationId = formIdStr ? id(`${formIdStr}_notification`) : null;
|
|
43
96
|
|
|
44
|
-
if (!
|
|
45
|
-
throw new Error('
|
|
97
|
+
if (!notificationId && !isOptionsObj) {
|
|
98
|
+
throw new Error('Notification element not found');
|
|
46
99
|
}
|
|
47
100
|
|
|
48
|
-
|
|
101
|
+
if (notificationId) {
|
|
102
|
+
notificationId.style.display = 'none';
|
|
103
|
+
['is-danger', 'is-success', 'w3-red', 'w3-green', 'bg-danger', 'bg-success'].forEach(cls =>
|
|
104
|
+
notificationId.classList.remove(cls)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
49
107
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
if (
|
|
108
|
+
// Build form data
|
|
109
|
+
let formEntries;
|
|
110
|
+
if (form instanceof FormData) {
|
|
111
|
+
formEntries = form;
|
|
112
|
+
} else if (form instanceof HTMLFormElement) {
|
|
113
|
+
formEntries = new FormData(form);
|
|
114
|
+
} else {
|
|
115
|
+
formEntries = new FormData();
|
|
116
|
+
}
|
|
53
117
|
|
|
54
|
-
|
|
55
|
-
|
|
118
|
+
// Append recaptcha token if present
|
|
119
|
+
if (recaptcha) {
|
|
120
|
+
formEntries.append('siteKey', recaptcha.token || '');
|
|
121
|
+
formEntries.append('action', recaptcha.action || 'SUBMIT');
|
|
56
122
|
}
|
|
57
123
|
|
|
58
124
|
formEntries.delete('submit');
|
|
59
125
|
formEntries.delete('checkbox');
|
|
60
126
|
|
|
61
127
|
const options = {
|
|
62
|
-
baseURL: '/',
|
|
63
|
-
withCredentials: true,
|
|
128
|
+
baseURL: '/',
|
|
129
|
+
withCredentials: true,
|
|
64
130
|
};
|
|
65
131
|
|
|
66
|
-
// AXIOS POST FUNCTIONALITY
|
|
67
132
|
try {
|
|
68
133
|
const response = await axios.post(url, formEntries, options);
|
|
69
|
-
const successClass = getNotificationClassByCSS(
|
|
134
|
+
const successClass = getNotificationClassByCSS(themeCss || 'bulma', 'green');
|
|
70
135
|
|
|
71
|
-
|
|
72
|
-
const { message } = response.data || {};
|
|
136
|
+
const { message } = response?.data || {};
|
|
73
137
|
if (!message) {
|
|
74
138
|
throw new Error('Response data does not contain a message');
|
|
75
139
|
}
|
|
76
|
-
|
|
140
|
+
|
|
141
|
+
const successMsg = typeof message === 'string' && message.trim()
|
|
142
|
+
? message.trim()
|
|
143
|
+
: 'Operation completed successfully.';
|
|
144
|
+
|
|
145
|
+
// Modern SweetAlert2 toast notification
|
|
146
|
+
swalSuccess(successMsg, true);
|
|
147
|
+
|
|
148
|
+
// Fallback DOM notification if element exists
|
|
149
|
+
if (notificationId) {
|
|
150
|
+
processFormDataAction(successClass, successMsg, notificationId);
|
|
151
|
+
}
|
|
77
152
|
|
|
78
153
|
if (redirect) {
|
|
79
|
-
redirectAfterDelay(redirect,
|
|
154
|
+
redirectAfterDelay(redirect, 1500);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (shouldResetForm && typeof form.reset === 'function') {
|
|
158
|
+
form.reset();
|
|
80
159
|
}
|
|
81
160
|
|
|
82
|
-
|
|
161
|
+
return response;
|
|
83
162
|
|
|
84
163
|
} catch (error) {
|
|
85
|
-
const errorClass = getNotificationClassByCSS(
|
|
86
|
-
const
|
|
87
|
-
|
|
164
|
+
const errorClass = getNotificationClassByCSS(themeCss || 'bulma', 'red');
|
|
165
|
+
const errorMsg = parseErrorResponse(error) || 'An error occurred during submission.';
|
|
166
|
+
|
|
167
|
+
// Modern SweetAlert2 error modal/toast
|
|
168
|
+
swalError(errorMsg, true);
|
|
169
|
+
|
|
170
|
+
// Fallback DOM notification if element exists
|
|
171
|
+
if (notificationId) {
|
|
172
|
+
processFormDataAction(errorClass, errorMsg, notificationId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { error: true, message: errorMsg, rawError: error };
|
|
176
|
+
|
|
177
|
+
} finally {
|
|
178
|
+
// Release lock
|
|
179
|
+
releaseLock(lockKey);
|
|
180
|
+
|
|
181
|
+
// Guaranteed spinner cleanup
|
|
182
|
+
if (btnEl) {
|
|
183
|
+
clearButtonLoading(btnEl);
|
|
184
|
+
}
|
|
185
|
+
if (loader) {
|
|
186
|
+
loader.style.display = 'none';
|
|
187
|
+
loader.classList.remove('loader');
|
|
188
|
+
}
|
|
88
189
|
}
|
|
89
190
|
};
|
|
90
191
|
|
|
@@ -167,11 +268,14 @@ export const getApiData = async (URL, token = null) => {
|
|
|
167
268
|
headers: {
|
|
168
269
|
"X-Requested-With": "XMLHttpRequest",
|
|
169
270
|
"Content-Type": "application/json",
|
|
170
|
-
Accept: "application/json"
|
|
171
|
-
...(token && { Authorization: `Bearer ${token}` }), // only add if token exists
|
|
271
|
+
Accept: "application/json"
|
|
172
272
|
},
|
|
173
273
|
};
|
|
174
274
|
|
|
275
|
+
if (token) {
|
|
276
|
+
config.headers['Authorization'] = `Bearer ${token}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
175
279
|
const response = await axios.get(URL, config);
|
|
176
280
|
return response.data;
|
|
177
281
|
} catch (error) {
|
|
@@ -208,11 +312,14 @@ export const getMultipleApiData = async (url1, url2, token = null) => {
|
|
|
208
312
|
headers: {
|
|
209
313
|
"X-Requested-With": "XMLHttpRequest",
|
|
210
314
|
"Content-Type": "application/json",
|
|
211
|
-
Accept: "application/json"
|
|
212
|
-
...(token && { Authorization: `Bearer ${token}` }), // add only if token exists
|
|
315
|
+
Accept: "application/json"
|
|
213
316
|
},
|
|
214
317
|
};
|
|
215
318
|
|
|
319
|
+
if (token) {
|
|
320
|
+
config.headers['Authorization'] = `Bearer ${token}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
216
323
|
// run requests in parallel
|
|
217
324
|
const [res1, res2] = await axios.all([
|
|
218
325
|
axios.get(url1, config),
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { id } from './UtilityHtml.js';
|
|
3
|
+
|
|
4
|
+
const VALID_EMAIL_REGEX = /^[\w\-\.\+]+\@[a-zA-Z0-9\.\-]+\.[a-zA-Z0-9]{2,6}$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attaches real-time validation and micro-feedback to form fields on blur/input.
|
|
8
|
+
*
|
|
9
|
+
* @param {string|HTMLElement} fieldId - ID or DOM element of the input
|
|
10
|
+
* @param {Object} options
|
|
11
|
+
* @param {'email'|'password'|'required'|'custom'} [options.type='required']
|
|
12
|
+
* @param {(value: string) => { valid: boolean, message?: string }} [options.validator]
|
|
13
|
+
* @param {string} [options.errorColor='#e74c3c']
|
|
14
|
+
* @param {string} [options.successColor='#27ae60']
|
|
15
|
+
*/
|
|
16
|
+
export const attachInlineValidation = (fieldId, options = {}) => {
|
|
17
|
+
if (typeof document === 'undefined') return;
|
|
18
|
+
|
|
19
|
+
const el = typeof fieldId === 'string' ? id(fieldId) : fieldId;
|
|
20
|
+
if (!el) return;
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
type = 'required',
|
|
24
|
+
validator = null,
|
|
25
|
+
errorColor = '#e74c3c',
|
|
26
|
+
successColor = '#27ae60'
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
const errorElId = `${el.id || el.name}_error`;
|
|
30
|
+
let feedbackEl = id(errorElId);
|
|
31
|
+
|
|
32
|
+
const validate = () => {
|
|
33
|
+
const val = (el.value || '').trim();
|
|
34
|
+
let isValid = true;
|
|
35
|
+
let message = '';
|
|
36
|
+
|
|
37
|
+
if (typeof validator === 'function') {
|
|
38
|
+
const result = validator(val);
|
|
39
|
+
isValid = !!result.valid;
|
|
40
|
+
message = result.message || '';
|
|
41
|
+
} else if (type === 'required') {
|
|
42
|
+
isValid = val.length > 0;
|
|
43
|
+
message = isValid ? '' : 'This field is required.';
|
|
44
|
+
} else if (type === 'email') {
|
|
45
|
+
isValid = VALID_EMAIL_REGEX.test(val);
|
|
46
|
+
message = isValid ? '' : 'Please enter a valid email address.';
|
|
47
|
+
} else if (type === 'password') {
|
|
48
|
+
isValid = val.length >= 8;
|
|
49
|
+
message = isValid ? '' : 'Password must be at least 8 characters long.';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Apply micro-feedback styling
|
|
53
|
+
if (isValid) {
|
|
54
|
+
el.style.borderColor = val ? successColor : '';
|
|
55
|
+
if (feedbackEl) {
|
|
56
|
+
feedbackEl.innerHTML = '';
|
|
57
|
+
feedbackEl.style.display = 'none';
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
el.style.borderColor = errorColor;
|
|
61
|
+
if (feedbackEl) {
|
|
62
|
+
feedbackEl.style.display = 'block';
|
|
63
|
+
feedbackEl.style.color = errorColor;
|
|
64
|
+
feedbackEl.innerHTML = message;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return isValid;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
el.addEventListener('blur', validate);
|
|
72
|
+
el.addEventListener('input', () => {
|
|
73
|
+
// Clear error border while user is re-typing
|
|
74
|
+
if (el.style.borderColor === errorColor) {
|
|
75
|
+
validate();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
package/NetworkGuard.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { swalToast } from './Alert.js';
|
|
3
|
+
|
|
4
|
+
let isListening = false;
|
|
5
|
+
let wasOffline = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks if the browser is currently online.
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export const isOnline = () => {
|
|
12
|
+
return typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'
|
|
13
|
+
? navigator.onLine
|
|
14
|
+
: true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Attaches automatic online/offline listeners to show clean toasts.
|
|
19
|
+
* Safe to call multiple times (idempotent).
|
|
20
|
+
*/
|
|
21
|
+
export const setupNetworkListeners = () => {
|
|
22
|
+
if (typeof window === 'undefined' || isListening) return;
|
|
23
|
+
isListening = true;
|
|
24
|
+
|
|
25
|
+
window.addEventListener('offline', () => {
|
|
26
|
+
wasOffline = true;
|
|
27
|
+
swalToast('Internet connection lost. You are offline.', 'warning', 4000);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
window.addEventListener('online', () => {
|
|
31
|
+
if (wasOffline) {
|
|
32
|
+
swalToast('Connection restored. Back online!', 'success', 3000);
|
|
33
|
+
wasOffline = false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wraps an async request to check connectivity before dispatching.
|
|
40
|
+
*
|
|
41
|
+
* @param {() => Promise<any>} requestFn
|
|
42
|
+
* @param {string} [offlineMsg='No internet connection. Please check your network.']
|
|
43
|
+
* @returns {Promise<any>}
|
|
44
|
+
*/
|
|
45
|
+
export const withNetworkGuard = async (
|
|
46
|
+
requestFn,
|
|
47
|
+
offlineMsg = 'No internet connection. Please check your network.'
|
|
48
|
+
) => {
|
|
49
|
+
if (!isOnline()) {
|
|
50
|
+
swalToast(offlineMsg, 'warning', 4000);
|
|
51
|
+
throw new Error(offlineMsg);
|
|
52
|
+
}
|
|
53
|
+
return await requestFn();
|
|
54
|
+
};
|
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.
|