@modernman00/shared-js-lib 1.2.49 → 1.2.60

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.
@@ -2,6 +2,7 @@ import { loginSubmission } from './processAll.js';
2
2
  import { id } from '../UtilityHtml.js';
3
3
  import { bindEvent, showPassword } from '../Utility.js';
4
4
  import { matchInput } from '../general.js';
5
+ import { showError } from '../ShowResponse.js';
5
6
 
6
7
 
7
8
  /**
@@ -26,6 +27,7 @@ import { matchInput } from '../general.js';
26
27
  * @param {string} [config.redirect] - URL to redirect to after successful password change.
27
28
  * @param {string} [config.theme='bulma'] - UI theme identifier passed to `loginSubmission`.
28
29
  * @param {number} [config.maxLength=50] - Maximum allowed length for password fields.
30
+ * @param {Object} [config.recaptchaV3=null] - reCAPTCHA v3 configuration ({ siteKey: 'xxx', action: 'CHANGE_PASSWORD' }).
29
31
  *
30
32
  * @example
31
33
  * setupPasswordChange(); // Uses default IDs and routes
@@ -52,7 +54,8 @@ export const setupPasswordChange = ({
52
54
  route,
53
55
  redirect,
54
56
  theme = 'bulma',
55
- maxLength = 50
57
+ maxLength = 50,
58
+ recaptchaV3 = null // { siteKey: 'xxx', action: 'CHANGE_PASSWORD' }
56
59
  } = {}) => {
57
60
  // Accessibility & input attributes
58
61
  const passwordInput = id(passwordId);
@@ -65,6 +68,8 @@ export const setupPasswordChange = ({
65
68
  helperText.textContent =
66
69
  'Password must be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one number, and one special character.';
67
70
 
71
+
72
+
68
73
  // Length limit object
69
74
  const lengthLimit = {
70
75
  maxLength: {
@@ -74,9 +79,24 @@ export const setupPasswordChange = ({
74
79
  };
75
80
 
76
81
  // Submission handler
77
- const handleSubmit = (e) => {
82
+ const handleSubmit = async (e) => {
78
83
  e.preventDefault();
79
- loginSubmission(formId, route, redirect, theme, lengthLimit);
84
+
85
+ let recaptchaToken = null;
86
+ // Only run v3 if configured AND grecaptcha is available
87
+ if (recaptchaV3?.siteKey && window.grecaptcha && window.grecaptcha.enterprise) {
88
+ try {
89
+ recaptchaToken = await grecaptcha.enterprise.execute(
90
+ recaptchaV3.siteKey,
91
+ { action: recaptchaV3.action || 'CHANGE_PASSWORD' });
92
+ } catch (err) {
93
+ showError(err);
94
+ }
95
+ }
96
+
97
+ loginSubmission(formId, route, redirect, theme, lengthLimit, [], recaptchaToken);
98
+
99
+
80
100
  };
81
101
 
82
102
  // Bind events
package/AcctMgt/code.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { loginSubmission } from './processAll.js';
2
2
  import { bindEvent } from '../Utility.js';
3
+ import { showError } from '../ShowResponse.js';
3
4
 
4
5
  /**
5
6
  * Creates a reusable form submission handler for code-based forms.
@@ -13,6 +14,7 @@ import { bindEvent } from '../Utility.js';
13
14
  * @param {string} options.redirect - URL to redirect to after success.
14
15
  * @param {string} [options.theme='bulma'] - UI theme identifier.
15
16
  * @param {number} [options.maxLength=50] - Max input length.
17
+ * @param {Object} [options.recaptchaV3=null] - reCAPTCHA v3 configuration ({ siteKey: 'OUR_ENTERPRISE_SITE_KEYx', action: 'TOKEN' }).
16
18
  *
17
19
  * @example
18
20
  * createCodeSubmitHandler({
@@ -30,11 +32,24 @@ export function createCodeSubmitHandler({
30
32
  route,
31
33
  redirect,
32
34
  theme = 'bulma',
33
- maxLength = 50
35
+ maxLength = 50,
36
+ recaptchaV3 = null // { siteKey: 'xxx', action: 'LOGIN' }
34
37
  }) {
35
- const handler = (e) => {
38
+ const handler = async (e) => {
36
39
  e.preventDefault();
37
40
 
41
+ let recaptchaToken = null;
42
+ // Only run v3 if configured AND grecaptcha is available
43
+ if (recaptchaV3?.siteKey && window.grecaptcha && window.grecaptcha.enterprise) {
44
+ try {
45
+ recaptchaToken = await grecaptcha.enterprise.execute(
46
+ recaptchaV3.siteKey,
47
+ { action: recaptchaV3.action || 'LOGIN' });
48
+ } catch (err) {
49
+ showError(err);
50
+ }
51
+ }
52
+
38
53
  const lengthLimit = {
39
54
  maxLength: {
40
55
  id: [inputId],
package/AcctMgt/forgot.js CHANGED
@@ -1,5 +1,6 @@
1
- import { loginSubmission} from './processAll.js';
2
- import {bindEvent } from '../Utility.js';
1
+ import { loginSubmission } from './processAll.js';
2
+ import { bindEvent } from '../Utility.js';
3
+ import { showError } from '../ShowResponse.js';
3
4
 
4
5
 
5
6
  /**
@@ -19,6 +20,7 @@ import {bindEvent } from '../Utility.js';
19
20
  * @param {string} options.redirect - URL to redirect to after success.
20
21
  * @param {string} [options.theme='bulma'] - UI theme identifier.
21
22
  * @param {number} [options.maxLength=50] - Max input length.
23
+ * @param {Object} [options.recaptchaV3=null] - reCAPTCHA v3 configuration ({ siteKey: 'OUR_ENTERPRISE_SITE_KEYx', action: 'FORGOT' }).
22
24
  *
23
25
  * @example
24
26
  * createEmailSubmitHandler({
@@ -36,11 +38,24 @@ export function forgotSubmitHandler({
36
38
  route,
37
39
  redirect,
38
40
  theme = 'bulma',
39
- maxLength = 50
41
+ maxLength = 50,
42
+ recaptchaV3 = null // { siteKey: 'xxx', action: 'LOGIN' }
40
43
  }) {
41
- const handler = (e) => {
44
+ const handler = async (e) => {
42
45
  e.preventDefault();
43
46
 
47
+ let recaptchaToken = null;
48
+ // Only run v3 if configured AND grecaptcha is available
49
+ if (recaptchaV3?.siteKey && window.grecaptcha && window.grecaptcha.enterprise) {
50
+ try {
51
+ recaptchaToken = await grecaptcha.enterprise.execute(
52
+ recaptchaV3.siteKey,
53
+ { action: recaptchaV3.action || 'LOGIN' });
54
+ } catch (err) {
55
+ showError(err);
56
+ }
57
+ }
58
+
44
59
  const lengthLimit = {
45
60
  maxLength: {
46
61
  id: [inputId],
package/AcctMgt/login.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { loginSubmission } from './processAll.js';
2
2
  import { id} from '../UtilityHtml.js';
3
3
  import { bindEvent, showPassword } from '../Utility.js';
4
+ import { showError } from '../ShowResponse.js';
4
5
 
5
6
 
6
7
  /**
@@ -23,6 +24,7 @@ import { bindEvent, showPassword } from '../Utility.js';
23
24
  * @param {string} options.redirect - URL to redirect to after successful login.
24
25
  * @param {string} [options.theme='bootstrap'] - UI theme identifier.
25
26
  * @param {number[]} [options.maxLength=[30, 50]] - Max lengths for password and email respectively.
27
+ * @param {Object} [options.recaptchaV3=null] - reCAPTCHA v3 configuration ({ siteKey: 'OUR_ENTERPRISE_SITE_KEY', action: 'LOGIN' }).
26
28
  *
27
29
  * @example
28
30
  * createAdminLoginHandler({
@@ -44,11 +46,24 @@ export function createAdminLoginHandler({
44
46
  route,
45
47
  redirect,
46
48
  theme = 'bootstrap',
47
- maxLength = [30, 50]
49
+ maxLength = [30, 50],
50
+ recaptchaV3 = null // { siteKey: 'xxx', action: 'LOGIN' }
48
51
  }) {
49
- const handler = (e) => {
52
+ const handler = async (e) => {
50
53
  e.preventDefault();
51
54
 
55
+ let recaptchaToken = null;
56
+ // Only run v3 if configured AND grecaptcha is available
57
+ if (recaptchaV3?.siteKey && window.grecaptcha && window.grecaptcha.enterprise) {
58
+ try {
59
+ recaptchaToken = await grecaptcha.enterprise.execute(
60
+ recaptchaV3.siteKey,
61
+ { action: recaptchaV3.action || 'LOGIN' } );
62
+ } catch (err) {
63
+ showError(err);
64
+ }
65
+ }
66
+
52
67
  const lengthLimit = {
53
68
  maxLength: {
54
69
  id: [passwordId, emailId],
@@ -56,7 +71,7 @@ export function createAdminLoginHandler({
56
71
  }
57
72
  };
58
73
 
59
- loginSubmission(formId, route, redirect, theme, lengthLimit);
74
+ loginSubmission(formId, route, redirect, theme, lengthLimit, [], recaptchaToken);
60
75
  };
61
76
 
62
77
  bindEvent({ id: buttonId, handler });
@@ -9,15 +9,15 @@ import FormHelper from '../FormHelper.js';
9
9
  // block the setLoader div
10
10
 
11
11
  /**
12
- * Handles the submission of the login form.
12
+ * Handles the submission of the login form and redirects the user.
13
13
  * @param {string} formId - The ID of the form to submit.
14
14
  * @param {string} loginURL - The URL to make the POST request to.
15
15
  * @param {string} redirect - The URL to redirect the user to after the submission is complete.
16
16
  * @param {string} [css=null] - The CSS class to add to the notification element if the submission is successful.
17
- * @returns {void}
17
+ * @returns {void} yes
18
18
  * @throws {Error} - If there is an error with the submission
19
19
  */
20
- export const loginSubmission = async (formId, loginURL, redirect, css = null, lengthLimit = null
20
+ export const loginSubmission = async (formId, loginURL, redirect, css = null, lengthLimit = null, optionalFields = [], recaptchaToken = null
21
21
  ) => {
22
22
 
23
23
  try {
@@ -39,7 +39,7 @@ export const loginSubmission = async (formId, loginURL, redirect, css = null, le
39
39
  formData.realTimeCheckLen(lengthLimit.maxLength.id, lengthLimit.maxLength.max);
40
40
  }
41
41
 
42
- formData.massValidate([], { email: 'email', password: 'password' });
42
+ formData.massValidate(optionalFields, { email: 'email', password: 'password' });
43
43
 
44
44
  if (formData.result === 1) {
45
45
 
@@ -51,7 +51,7 @@ export const loginSubmission = async (formId, loginURL, redirect, css = null, le
51
51
 
52
52
  localStorage.setItem('redirect', redirect);
53
53
 
54
- await postFormData(loginURL, formId, redirect, css);
54
+ await postFormData(loginURL, formId, redirect, css, recaptchaToken);
55
55
 
56
56
  } else {
57
57
 
package/AcctMgt/update.js CHANGED
@@ -1,6 +1,6 @@
1
- import {id } from '../UtilityHtml.js';
1
+ import { id } from '../UtilityHtml.js';
2
2
  import { bindEvent } from '../Utility.js';
3
- import { msgException } from '../ShowResponse.js';
3
+ import { msgException, showError } from '../ShowResponse.js';
4
4
  import axios from 'axios';
5
5
  import { redirectAfterDelay, parseErrorResponse } from '../general.js';
6
6
  /**
@@ -12,6 +12,8 @@ import { redirectAfterDelay, parseErrorResponse } from '../general.js';
12
12
  * @param {string} options.buttonId - ID of the submit button.
13
13
  * @param {string} options.route - API endpoint for submission.
14
14
  * @param {string} options.redirect - URL to redirect to after success.
15
+ * @param {Object} [options.recaptchaV3=null] - reCAPTCHA v3 configuration ({ siteKey: 'OUR_ENTERPRISE_SITE_KEY', action: 'LOGIN' }).
16
+ *
15
17
  *
16
18
  * @example
17
19
  * update({
@@ -26,10 +28,23 @@ export function update({
26
28
  buttonId,
27
29
  route,
28
30
  redirect,
31
+ recaptchaV3 = null // { siteKey: 'xxx', action: 'UPDATE_DATA' }
29
32
  }) {
30
33
  const handler = async (e) => {
31
34
  e.preventDefault();
32
35
 
36
+ let recaptchaToken = null;
37
+ // Only run v3 if configured AND grecaptcha is available
38
+ if (recaptchaV3?.siteKey && window.grecaptcha && window.grecaptcha.enterprise) {
39
+ try {
40
+ recaptchaToken = await grecaptcha.enterprise.execute(
41
+ recaptchaV3.siteKey,
42
+ { action: recaptchaV3.action || 'LOGIN' });
43
+ } catch (err) {
44
+ showError(err);
45
+ }
46
+ }
47
+
33
48
  const form = id(formId);
34
49
  if (!form) {
35
50
  msgException(`Form with ID "${formId}" not found.`);
@@ -39,6 +54,11 @@ export function update({
39
54
  const formData = new FormData();
40
55
  const inputs = form.querySelectorAll('[name]');
41
56
 
57
+ // if recaptcha token is provided, append it
58
+ if (recaptchaToken) {
59
+ formData.append('recaptchaTokenV3', recaptchaToken);
60
+ }
61
+
42
62
  inputs.forEach(input => {
43
63
  const name = input.name;
44
64
  const original = input.getAttribute('data-original');
@@ -59,6 +79,7 @@ export function update({
59
79
  const message = response.data.message;
60
80
 
61
81
  const notificationId = id(`${formId}_notification`) || formId;
82
+ notificationId.style.display = 'block'
62
83
 
63
84
  notificationId.scrollIntoView({ behavior: 'smooth' });
64
85
 
@@ -73,6 +94,7 @@ export function update({
73
94
  console.log(error);
74
95
  const ErrorMsg = parseErrorResponse(error);
75
96
  const notificationId = id(`${formId}_notification`)
97
+ notificationId.style.display = 'block'
76
98
  notificationId.innerHTML = `<li style="color:white; background-color:red">${ErrorMsg}</li>`;
77
99
  }
78
100
  };
package/FormHelper.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  export default class FormHelper {
3
3
  constructor(data) {
4
- if (!Array.isArray(data)) throwError('data must be an array of form elements');
4
+ if (!Array.isArray(data)) throw new Error('data must be an array of form elements');
5
5
  this.data = data;
6
6
  this.error = [];
7
7
  this.result = 0;
@@ -101,7 +101,7 @@ export default class FormHelper {
101
101
 
102
102
 
103
103
 
104
- massValidate(optionalFields = [], typeMap = {}) {
104
+ massValidate(optionalFields, typeMap = {}) {
105
105
  this.clearError(); // Reset errors
106
106
  this.result = 0;
107
107
 
@@ -109,11 +109,11 @@ export default class FormHelper {
109
109
  const { name, value, id, type, placeholder } = input;
110
110
 
111
111
 
112
- // Skip non-input elements
112
+ // Skip non-input elements
113
113
  if (
114
114
  ['submit', 'button', 'g-recaptcha-response', 'cancel'].includes(name) ||
115
115
  ['button', 'showPassword', 'token', 'g-recaptcha-response'].includes(id) ||
116
- type === 'button' ||
116
+ type === 'button' || optionalFields.includes(id) ||
117
117
  id === 'checkbox'
118
118
  ) continue;
119
119
 
@@ -123,7 +123,7 @@ export default class FormHelper {
123
123
  const errorEl = this.id(`${id}_error`);
124
124
  let val = value.trim();
125
125
 
126
- // Handle optional fields
126
+ // Handle optional fields - If optional but empty → auto‑fill “Not Provided”
127
127
  if (optionalFields.includes(id) && val === '') {
128
128
  input.value = 'Not Provided';
129
129
  continue;
package/Http.js CHANGED
@@ -11,7 +11,7 @@ import { redirectAfterDelay, parseErrorResponse } from './general.js';
11
11
  * @param {string|null} css - The CSS framework to use for notification styling (e.g., 'W3css', 'bulma').
12
12
  NOTICE:::Make sure you set the notification id as the formId_notification
13
13
  */
14
- export const postFormData = async (url, formId, redirect = null, css = null) => {
14
+ export const postFormData = async (url, formId, redirect = null, css = null, recaptchaToken = null) => {
15
15
 
16
16
  let notificationForm = `${formId}_notification`;
17
17
  const notificationId = id(notificationForm);
@@ -43,6 +43,11 @@ export const postFormData = async (url, formId, redirect = null, css = null) =>
43
43
 
44
44
  let formEntries = new FormData(form);
45
45
 
46
+ // if recaptcha token is provided, append it for v3
47
+ if (recaptchaToken) {
48
+ formEntries.append('recaptchaTokenV3', recaptchaToken);
49
+ }
50
+
46
51
  formEntries.delete('submit');
47
52
  formEntries.delete('checkbox');
48
53
 
@@ -1,3 +1,3 @@
1
1
  <?php
2
- define('APP_VERSION', 'v1.2.50');
2
+ define('APP_VERSION', 'v1.2.60');
3
3
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modernman00/shared-js-lib",
3
- "version": "1.2.49",
3
+ "version": "1.2.60",
4
4
  "description": "Reusable JS utilities for numerous js problems",
5
5
  "homepage": "https://github.com/modernman00/shared-js-lib#readme",
6
6
  "keywords": [
@@ -23,24 +23,44 @@
23
23
  "test": "jest"
24
24
  },
25
25
  "dependencies": {
26
- "axios": "^1.9.0",
27
- "axios-retry": "^4.5.0"
26
+ "axios": "^1.13.2",
27
+ "axios-retry": "^4.5.0",
28
+ "settings": "^0.1.1"
28
29
  },
29
30
  "publishConfig": {
30
31
  "access": "public"
31
32
  },
33
+ "files": [
34
+ "index.js",
35
+ "AcctMgt/",
36
+ "Cookie.js",
37
+ "CountryCode.js",
38
+ "DateTime.js",
39
+ "FileUpload.js",
40
+ "FormHelper.js",
41
+ "Http.js",
42
+ "Loader.js",
43
+ "ShowResponse.js",
44
+ "Utility.js",
45
+ "UtilityHtml.js",
46
+ "axiosWrapper.js",
47
+ "general.js",
48
+ "config/",
49
+ "README.md",
50
+ "release-history.md"
51
+ ],
32
52
  "devDependencies": {
33
53
  "@babel/core": "^7.28.3",
34
54
  "@babel/preset-env": "^7.28.3",
35
55
  "@semantic-release/changelog": "^6.0.3",
36
56
  "@semantic-release/git": "^10.0.1",
37
57
  "@semantic-release/github": "^11.0.4",
38
- "@semantic-release/npm": "^12.0.2",
58
+ "@semantic-release/npm": "^13.1.2",
39
59
  "auto-changelog": "^2.5.0",
40
60
  "babel-jest": "^30.0.5",
41
61
  "jest": "^30.0.5",
42
62
  "jest-environment-jsdom": "^30.0.5",
43
- "semantic-release": "^24.2.7"
63
+ "semantic-release": "^25.0.2"
44
64
  },
45
65
  "release": {
46
66
  "branches": [
@@ -19,3 +19,15 @@
19
19
  ### Changelog for v1.2.18
20
20
 
21
21
  - 🔖 Bump version to v1.2.18 _(by modernman00)_
22
+
23
+ ## v1.2.59 — 2025-12-21 21:50
24
+ ### Changelog for v1.2.59
25
+
26
+ - 🔖 Bump version to v1.2.59 _(by modernman00)_
27
+ - 🧹 Auto-commit before release _(by modernman00)_
28
+ - 🔖 Bump version to v1.2.58 _(by modernman00)_
29
+ - 🧹 Auto-commit before release _(by modernman00)_
30
+ - 🧹 Auto-commit before release _(by modernman00)_
31
+ - 🧹 Auto-commit before release _(by modernman00)_
32
+ - 🧹 Auto-commit before release _(by modernman00)_
33
+ - 🧹 Auto-commit before release _(by modernman00)_
@@ -1,53 +0,0 @@
1
- name: Release shared-js-lib
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- release:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v3
15
-
16
- - name: Setup Node.js
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: '18'
20
- registry-url: 'https://registry.npmjs.org/'
21
- scope: '@modernman00'
22
-
23
- - name: Install dependencies
24
- run: npm ci
25
-
26
- - name: Run tests
27
- run: npm test
28
-
29
- - name: Bump version and generate changelog
30
- run: |
31
- npm version patch -m "chore(release): %s"
32
- npx auto-changelog -p
33
- env:
34
- GIT_AUTHOR_NAME: github-actions
35
- GIT_AUTHOR_EMAIL: actions@github.com
36
- GIT_COMMITTER_NAME: github-actions
37
- GIT_COMMITTER_EMAIL: actions@github.com
38
-
39
- - name: Push version bump and changelog
40
- run: |
41
- git push origin main --follow-tags
42
-
43
- - name: Publish to npm
44
- run: npm publish --access public
45
- env:
46
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
47
-
48
- - name: Create GitHub Release
49
- uses: softprops/action-gh-release@v1
50
- with:
51
- tag_name: ${{ github.ref_name }}
52
- env:
53
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,40 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*.*.*' # Trigger only on semantic version tags
7
-
8
- jobs:
9
- release:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v3
15
-
16
- - name: Use Node.js
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: '18'
20
- registry-url: 'https://registry.npmjs.org/'
21
-
22
- - name: Install dependencies
23
- run: npm ci
24
-
25
- - name: Publish to npm
26
- run: npm publish --access public
27
- env:
28
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
29
-
30
- - name: Confirm publish
31
- run: |
32
- echo "🔍 Verifying publish..."
33
- PACKAGE=$(node -p "require('./package.json').name")
34
- VERSION=$(node -p "require('./package.json').version")
35
- PUBLISHED=$(npm view $PACKAGE version)
36
- if [[ "$PUBLISHED" == "$VERSION" ]]; then
37
- echo "✅ $PACKAGE@$VERSION is live on npmjs.org"
38
- else
39
- echo "⚠️ Expected $VERSION, but registry shows $PUBLISHED"
40
- fi
@@ -1,32 +0,0 @@
1
- name: Run Tests and Coverage
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout repo
15
- uses: actions/checkout@v3
16
-
17
- - name: Setup Node.js
18
- uses: actions/setup-node@v3
19
- with:
20
- node-version: '20'
21
-
22
- - name: Install dependencies
23
- run: npm ci
24
-
25
- - name: Run tests with coverage
26
- run: npm test -- --coverage
27
-
28
- - name: Upload coverage report
29
- uses: actions/upload-artifact@v3
30
- with:
31
- name: coverage-report
32
- path: coverage/
@@ -1,156 +0,0 @@
1
- import { createCodeSubmitHandler } from '../AcctMgt/code.js';
2
- import { forgotSubmitHandler } from '../AcctMgt/forgot.js';
3
- import { createAdminLoginHandler } from '../AcctMgt/login.js';
4
- import { setupPasswordChange } from '../AcctMgt/changePassword.js';
5
- import { loginSubmission } from '../AcctMgt/processAll.js';
6
- import { showPassword, bindEvent } from '../Utility.js';
7
-
8
-
9
- jest.mock('../AcctMgt/processAll.js');
10
- jest.mock('../Utility.js');
11
-
12
- // Then patch the specific function after DOM is available
13
- beforeEach(() => {
14
- bindEvent.mockImplementation(({ id, event = 'click', handler }) => {
15
- const el = document.getElementById(id);
16
- if (el) el.addEventListener(event, handler);
17
- });
18
-
19
- showPassword.mockImplementation(() => {});
20
- });
21
-
22
- // 🧪 Utility to scaffold DOM elements
23
- const injectDOM = (elements) => {
24
- elements.forEach(({ tag = 'button', id, value = '' }) => {
25
- const el = document.createElement(tag);
26
- el.setAttribute('id', id);
27
- if (value) el.value = value;
28
- document.body.appendChild(el);
29
- });
30
- };
31
-
32
- afterEach(() => {
33
- document.body.innerHTML = '';
34
- jest.clearAllMocks();
35
- });
36
-
37
- describe('AcctMgt Handlers', () => {
38
- test('🧾 createCodeSubmitHandler submits code form correctly', () => {
39
- injectDOM([{ id: 'button' }]);
40
-
41
- createCodeSubmitHandler({
42
- formId: 'codeForm',
43
- inputId: 'code',
44
- buttonId: 'button',
45
- route: '/api/code',
46
- redirect: '/success',
47
- theme: 'bulma',
48
- maxLength: 40
49
- });
50
-
51
- document.getElementById('button').dispatchEvent(new Event('click'));
52
-
53
- expect(loginSubmission).toHaveBeenCalledWith(
54
- 'codeForm',
55
- '/api/code',
56
- '/success',
57
- 'bulma',
58
- { maxLength: { id: ['code'], max: [40] } }
59
- );
60
- });
61
-
62
- test('📧 forgotSubmitHandler submits email form correctly', () => {
63
- injectDOM([
64
- { id: 'button' },
65
- { tag: 'input', id: 'email', value: 'user@example.com' }
66
- ]);
67
-
68
- forgotSubmitHandler({
69
- formId: 'forgotPassword',
70
- inputId: 'email',
71
- buttonId: 'button',
72
- route: '/api/forgot',
73
- redirect: '/reset',
74
- theme: 'bulma',
75
- maxLength: 50
76
- });
77
-
78
- document.getElementById('button').dispatchEvent(new Event('click'));
79
-
80
- expect(loginSubmission).toHaveBeenCalledWith(
81
- 'forgotPassword',
82
- '/api/forgot',
83
- '/reset',
84
- 'bulma',
85
- { maxLength: { id: ['email'], max: [50] } }
86
- );
87
- });
88
-
89
- test('🔐 createAdminLoginHandler sets attributes and submits login', () => {
90
- injectDOM([
91
- { id: 'button' },
92
- { id: 'showPassword' },
93
- { tag: 'input', id: 'password' }
94
- ]);
95
-
96
- createAdminLoginHandler({
97
- formId: 'managed',
98
- emailId: 'email',
99
- passwordId: 'password',
100
- buttonId: 'button',
101
- showToggleId: 'showPassword',
102
- route: '/api/login',
103
- redirect: '/admin',
104
- theme: 'bootstrap',
105
- maxLength: [30, 50]
106
- });
107
-
108
- document.getElementById('button').dispatchEvent(new Event('click'));
109
- document.getElementById('showPassword').dispatchEvent(new Event('click'));
110
-
111
- expect(loginSubmission).toHaveBeenCalledWith(
112
- 'managed',
113
- '/api/login',
114
- '/admin',
115
- 'bootstrap',
116
- { maxLength: { id: ['password', 'email'], max: [30, 50] } }
117
- );
118
-
119
- expect(document.getElementById('password').getAttribute('autocomplete')).toBe('current-password');
120
- expect(showPassword).toHaveBeenCalledWith('password');
121
- });
122
-
123
- test('🔁 setupPasswordChange submits when passwords match', () => {
124
- injectDOM([
125
- { id: 'button' },
126
- { id: 'showPassword' },
127
- { tag: 'input', id: 'password', value: 'Secure123!' },
128
- { tag: 'input', id: 'confirm_password', value: 'Secure123!' },
129
- { tag: 'div', id: 'confirm_password_error' },
130
- { tag: 'div', id: 'password_help' }
131
- ]);
132
-
133
- setupPasswordChange({
134
- buttonId: 'button',
135
- passwordId: 'password',
136
- confirmId: 'confirm_password',
137
- errorId: 'confirm_password_error',
138
- helpId: 'password_help',
139
- showToggleId: 'showPassword',
140
- route: '/api/change-password',
141
- redirect: '/dashboard',
142
- theme: 'bulma',
143
- maxLength: 50
144
- });
145
-
146
- document.getElementById('button').dispatchEvent(new Event('click'));
147
-
148
- expect(loginSubmission).toHaveBeenCalledWith(
149
- 'changePassword',
150
- '/api/change-password',
151
- '/dashboard',
152
- 'bulma',
153
- { maxLength: { id: ['password', 'confirm_password'], max: [50, 50] } }
154
- );
155
- });
156
- });
package/babel.config.js DELETED
@@ -1,3 +0,0 @@
1
- export default {
2
- presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
3
- };
package/jest.config.js DELETED
@@ -1,6 +0,0 @@
1
- export default {
2
- transform: {
3
- '^.+\\.js$': 'babel-jest'
4
- },
5
- testEnvironment: 'jsdom'
6
- };
package/release.sh DELETED
@@ -1,161 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # --- Config ---
5
- VERSION_FILE="config/version.php"
6
- HISTORY_FILE="release-history.md"
7
- REPO="origin"
8
- DEFAULT_BRANCH="main"
9
-
10
- # --- Utilities ---
11
- function detect_branch() {
12
- CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
13
- echo "🔍 Current branch: $CURRENT_BRANCH"
14
- if [[ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" && "$CURRENT_BRANCH" != "master" ]]; then
15
- echo "⚠️ You're on '$CURRENT_BRANCH'. Switch to '$DEFAULT_BRANCH' or 'master' before releasing."
16
- exit 1
17
- fi
18
- }
19
-
20
- function ensure_clean_git() {
21
- if [[ -n $(git status --porcelain) ]]; then
22
- echo "🛑 Uncommitted changes detected."
23
- git status
24
- read -p "❓ Auto-commit all changes before release? (y/n): " choice
25
- if [[ "$choice" == "y" ]]; then
26
- git add .
27
- git commit -m "🧹 Auto-commit before release"
28
- else
29
- echo "🚫 Aborting release."
30
- exit 1
31
- fi
32
- fi
33
- }
34
-
35
- function run_tests() {
36
- echo "🧪 Running tests with coverage..."
37
- npm test -- --coverage || {
38
- echo "❌ Tests failed. Aborting release."
39
- exit 1
40
- }
41
- echo "✅ Tests passed."
42
- }
43
-
44
- function validate_dependencies() {
45
- command -v gh >/dev/null || {
46
- echo "❌ GitHub CLI (gh) not found. Install it to publish releases."
47
- exit 1
48
- }
49
- }
50
-
51
- function extract_version() {
52
- grep -q "APP_VERSION" "$VERSION_FILE" || {
53
- echo "❌ APP_VERSION not found in $VERSION_FILE"
54
- exit 1
55
- }
56
- CURRENT=$(grep "APP_VERSION" "$VERSION_FILE" | sed -E "s/.*'([^']+)'.*/\1/")
57
- IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT//v/}"
58
- }
59
-
60
- function determine_bump_type() {
61
- LAST_COMMIT=$(git log -1 --pretty=%s)
62
- MODE="patch"
63
- [[ "$LAST_COMMIT" == feat:* ]] && MODE="minor"
64
- [[ "$LAST_COMMIT" == fix:* ]] && MODE="patch"
65
- [[ "$LAST_COMMIT" == chore:* || "$LAST_COMMIT" == docs:* ]] && MODE="patch"
66
- [[ "$LAST_COMMIT" == BREAKING:* ]] && MODE="major"
67
-
68
- case "$MODE" in
69
- major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
70
- minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
71
- patch) PATCH=$((PATCH + 1)) ;;
72
- esac
73
-
74
- SEMVER="${MAJOR}.${MINOR}.${PATCH}"
75
- NEW="v$SEMVER"
76
- echo "🔧 Current version: $CURRENT"
77
- echo "⏫ Detected bump: $MODE → $NEW"
78
- echo "📦 Last commit: $LAST_COMMIT"
79
- }
80
-
81
- function update_version_file() {
82
- if [[ "$OSTYPE" == "darwin"* ]]; then
83
- sed -i '' "s/$CURRENT/$NEW/" "$VERSION_FILE"
84
- else
85
- sed -i "s/$CURRENT/$NEW/" "$VERSION_FILE"
86
- fi
87
- }
88
-
89
- function commit_and_tag() {
90
- COMMIT_MSG="🔖 Bump version to $NEW"
91
- npm version "$SEMVER" --no-git-tag-version
92
- git add "$VERSION_FILE"
93
- git commit -m "$COMMIT_MSG"
94
-
95
- if git rev-parse "$NEW" >/dev/null 2>&1; then
96
- echo "⚠️ Tag $NEW already exists. Skipping tag creation."
97
- else
98
- git tag "$NEW"
99
- git push "$REPO" "$NEW"
100
- fi
101
-
102
- echo "🚀 Pushing changes to $REPO/$DEFAULT_BRANCH..."
103
- git push "$REPO" "$DEFAULT_BRANCH" || {
104
- echo "❌ Failed to push to $DEFAULT_BRANCH. Check your Git credentials or branch name."
105
- exit 1
106
- }
107
- }
108
-
109
- function generate_changelog() {
110
- LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
111
- [ "$LAST_TAG" = "$NEW" ] && LAST_TAG=$(git rev-list --tags --max-count=2 | tail -n1 | xargs git describe --tags --abbrev=0)
112
- NOTES="### Changelog for $NEW\n\n"
113
- NOTES+=$(git log "$LAST_TAG"..HEAD --pretty=format:"- %s _(by %an)_" --no-merges)
114
- [ -z "$NOTES" ] && NOTES="Initial release."
115
- }
116
-
117
- function publish_github_release() {
118
- gh release create "$NEW" \
119
- --title "Release $NEW" \
120
- --notes "$NOTES" \
121
- --verify-tag
122
- echo "✅ Published $NEW with changelog and updated $VERSION_FILE!"
123
- }
124
-
125
- function log_release_history() {
126
- echo -e "\n## $NEW — $(date '+%Y-%m-%d %H:%M')\n$NOTES" >> "$HISTORY_FILE"
127
- echo "📝 Logged release to $HISTORY_FILE"
128
- }
129
-
130
- function publish_to_npm() {
131
- echo "🚀 Publishing $SEMVER to npm..."
132
- npm publish --access public || {
133
- echo "❌ npm publish failed. Check your credentials, registry access, or package.json config."
134
- exit 1
135
- }
136
-
137
- echo "🔍 Verifying publish on npm registry..."
138
- PACKAGE_NAME=$(node -p "require('./package.json').name")
139
- PUBLISHED_VERSION=$(npm view "$PACKAGE_NAME" version)
140
-
141
- if [[ "$PUBLISHED_VERSION" == "$SEMVER" ]]; then
142
- echo "✅ npm publish confirmed: $PUBLISHED_VERSION is live on npmjs.org"
143
- else
144
- echo "⚠️ Publish may not have propagated yet. Expected $SEMVER, got $PUBLISHED_VERSION"
145
- fi
146
- }
147
-
148
-
149
- # --- Execution ---
150
- detect_branch
151
- ensure_clean_git
152
- run_tests
153
- validate_dependencies
154
- extract_version
155
- determine_bump_type
156
- update_version_file
157
- commit_and_tag
158
- generate_changelog
159
- publish_github_release
160
- log_release_history
161
- publish_to_npm
@@ -1,93 +0,0 @@
1
- import { id } from './UtilityHtml.js'
2
-
3
- /**
4
- *
5
- * @param {string} inputId
6
- * @param {Array} arr
7
- */
8
-
9
- /**
10
- * Sets up an autocomplete functionality for an input element.
11
- *
12
- * @param {string} inputId - The ID of the input element to attach the autocomplete to.
13
- * @param {Array<string>} arr - An array of strings containing the autocomplete suggestions.
14
- *
15
- * The function converts all strings in the array to lowercase for case-insensitive matching.
16
- * It initializes the autocomplete with the given input element and array of suggestions,
17
- * providing real-time suggestions based on user input. On selecting a suggestion,
18
- * it populates the input element with the selected suggestion.
19
- */
20
- export const autocomplete = (inputId, arr) => {
21
- const whatToBuyInput = id(inputId); // Get the text input
22
- if (whatToBuyInput) {
23
- // Create a <ul> for autocomplete suggestions
24
- const suggestionList = document.createElement("ul");
25
- suggestionList.classList.add("autocomplete-suggestions");
26
- suggestionList.id = "suggestions"; // For accessibility
27
- whatToBuyInput.parentElement.appendChild(suggestionList); // Append to input's parent
28
-
29
- // Function to show matching suggestions based on user input
30
- const showSuggestions = (inputValue) => {
31
- suggestionList.innerHTML = ""; // Clear previous suggestions
32
- if (!inputValue) return; // Exit if input is empty
33
-
34
- // Filter items that match the input (case-insensitive), limit to 8
35
- const matches = arr
36
- .filter(item => item.toLowerCase().includes(inputValue.toLowerCase()))
37
- .slice(0, 8);
38
-
39
- // Create <li> for each match
40
- matches.forEach((item, index) => {
41
- const li = document.createElement("li");
42
- li.textContent = item;
43
- li.setAttribute("tabindex", "0"); // Make focusable for keyboard
44
- li.setAttribute("data-index", index); // Store index for navigation
45
- li.addEventListener("click", () => {
46
- whatToBuyInput.value = item; // Set input value on click
47
- suggestionList.innerHTML = ""; // Clear suggestions
48
- });
49
- li.addEventListener("keypress", (e) => {
50
- if (e.key === "Enter") {
51
- whatToBuyInput.value = item; // Set input value on Enter
52
- suggestionList.innerHTML = ""; // Clear suggestions
53
- }
54
- });
55
- suggestionList.appendChild(li);
56
- });
57
- };
58
-
59
- // Show suggestions as user types
60
- whatToBuyInput.addEventListener("input", (e) => {
61
- showSuggestions(e.target.value);
62
- });
63
-
64
- // Clear suggestions on blur (with delay for click to register)
65
- whatToBuyInput.addEventListener("blur", () => {
66
- setTimeout(() => suggestionList.innerHTML = "", 200);
67
- });
68
-
69
- // Handle keyboard navigation for suggestions
70
- whatToBuyInput.addEventListener("keydown", (e) => {
71
- const suggestions = suggestionList.querySelectorAll("li");
72
- if (!suggestions.length) return; // Exit if no suggestions
73
-
74
- let focusedIndex = Array.from(suggestions).findIndex(li => li === document.activeElement);
75
- if (e.key === "ArrowDown") {
76
- e.preventDefault(); // Prevent cursor movement
77
- focusedIndex = (focusedIndex + 1) % suggestions.length; // Loop to start
78
- suggestions[focusedIndex].focus();
79
- } else if (e.key === "ArrowUp") {
80
- e.preventDefault();
81
- focusedIndex = (focusedIndex - 1 + suggestions.length) % suggestions.length; // Loop to end
82
- suggestions[focusedIndex].focus();
83
- } else if (e.key === "Enter" && focusedIndex >= 0) {
84
- e.preventDefault();
85
- whatToBuyInput.value = suggestions[focusedIndex].textContent; // Select item
86
- suggestionList.innerHTML = ""; // Clear suggestions
87
- } else if (e.key === "Escape") {
88
- suggestionList.innerHTML = ""; // Clear suggestions
89
- }
90
- });
91
- }
92
- }
93
-