@modernman00/shared-js-lib 1.2.76 → 1.2.78

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/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 class of the form.
10
- * @param {string|null} redirect - The page to redirect to after successful submission.
11
- * @param {string|null} css - The CSS framework to use for notification styling (e.g., 'W3css', 'bulma').
12
- NOTICE:::Make sure you set the notification id as the formId_notification
13
- * @param {Object} [options.recaptchaV3=null] - reCAPTCHA v3 configuration ({ siteKey: 'OUR_ENTERPRISE_SITE_KEY', action: 'LOGIN' }).
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 (url, formId, redirect = null, css = null, recaptchaToken = null) => {
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
- const loader = id('setLoader');
18
- if (loader) loader.style.display = 'block';
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
- let notificationForm = `${formId}_notification`;
21
- const notificationId = id(notificationForm);
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
- if (!notificationId) {
25
- throw new Error('Notification element not found');
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
- ['is-danger', 'is-success', 'w3-red', 'w3-green', 'bg-danger', 'bg-success'].forEach(cls => notificationId.classList.remove(cls));
82
+ // Set button spinner and disable button
83
+ if (btnEl) {
84
+ setButtonLoading(btnEl);
85
+ }
31
86
 
32
- // add value to the hidden csrf input field
33
- // const csrfToken = getCookieValue('XSRF-TOKEN');
34
- // if (csrfToken) {
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
- // extract the form entries
41
- // extract the form entriesËËË
42
- const form = id(formId)
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 (!form) {
45
- throw new Error('Form element not found');
97
+ if (!notificationId && !isOptionsObj) {
98
+ throw new Error('Notification element not found');
46
99
  }
47
100
 
48
- let formEntries = new FormData(form);
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
- // if recaptcha token is provided, append it for v3
51
- // if recaptcha token is provided, append it for v3
52
- if (recaptchaToken) {
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
- formEntries.append('siteKey', recaptchaToken.token);
55
- formEntries.append('action', recaptchaToken.action);
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: '/', // Adjust to your API base URL
63
- withCredentials: true, // Ensure cookies (e.g., XSRF token) are sent
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(css || 'bulma', 'green');
134
+ const successClass = getNotificationClassByCSS(themeCss || 'bulma', 'green');
70
135
 
71
- // check if response.data.message is an array
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
- processFormDataAction(successClass, message, notificationId);
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, 2000);
154
+ redirectAfterDelay(redirect, 1500);
155
+ }
156
+
157
+ if (shouldResetForm && typeof form.reset === 'function') {
158
+ form.reset();
80
159
  }
81
160
 
82
- form.reset();
161
+ return response;
83
162
 
84
163
  } catch (error) {
85
- const errorClass = getNotificationClassByCSS(css || 'bulma', 'red');
86
- const ErrorMsg = parseErrorResponse(error);
87
- processFormDataAction(errorClass, ErrorMsg, notificationId);
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
 
@@ -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
+ };
@@ -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.