@iamproperty/components 7.8.2--beta4 → 7.8.2--beta5

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.
Files changed (133) hide show
  1. package/assets/css/components/actionbar.component.css +1 -1
  2. package/assets/css/components/actionbar.component.css.map +1 -1
  3. package/assets/css/components/calendar.component.css +1 -1
  4. package/assets/css/components/calendar.component.css.map +1 -1
  5. package/assets/css/components/card.component.css +1 -1
  6. package/assets/css/components/card.component.css.map +1 -1
  7. package/assets/css/components/carousel.config.css +1 -1
  8. package/assets/css/components/carousel.config.css.map +1 -1
  9. package/assets/css/components/modal.component.css +1 -1
  10. package/assets/css/components/modal.component.css.map +1 -1
  11. package/assets/css/components/multi-step-modal.component.css +1 -1
  12. package/assets/css/components/multi-step-modal.component.css.map +1 -1
  13. package/assets/css/components/nav.component.css +1 -1
  14. package/assets/css/components/nav.component.css.map +1 -1
  15. package/assets/css/components/nav.global.css +1 -1
  16. package/assets/css/components/nav.global.css.map +1 -1
  17. package/assets/css/components/notification.global.css +1 -1
  18. package/assets/css/components/notification.global.css.map +1 -1
  19. package/assets/css/components/std-nav-standalone.component.css +1 -1
  20. package/assets/css/components/std-nav-standalone.component.css.map +1 -1
  21. package/assets/css/components/video-card.component.css +1 -1
  22. package/assets/css/components/video-card.component.css.map +1 -1
  23. package/assets/css/components/video-modal.component.css +1 -1
  24. package/assets/css/components/video-modal.component.css.map +1 -1
  25. package/assets/css/core.min.css +1 -1
  26. package/assets/css/core.min.css.map +1 -1
  27. package/assets/css/elements/dialog.css +1 -1
  28. package/assets/css/elements/dialog.css.map +1 -1
  29. package/assets/css/style.min.css +1 -1
  30. package/assets/css/style.min.css.map +1 -1
  31. package/assets/js/components/accordion/accordion.component.min.js +1 -1
  32. package/assets/js/components/actionbar/actionbar.component.min.js +3 -3
  33. package/assets/js/components/address-lookup/address-lookup.component.min.js +1 -1
  34. package/assets/js/components/advanced-select/advanced-select.component.min.js +1 -1
  35. package/assets/js/components/applied-filters/applied-filters.component.min.js +1 -1
  36. package/assets/js/components/banner/banner.component.min.js +1 -1
  37. package/assets/js/components/barchart/barchart.component.min.js +1 -1
  38. package/assets/js/components/bento-grid/bento-grid.component.min.js +1 -1
  39. package/assets/js/components/bone/bone.component.min.js +1 -1
  40. package/assets/js/components/button/button.component.min.js +1 -1
  41. package/assets/js/components/calendar/calendar.component.min.js +2 -2
  42. package/assets/js/components/card/card.component.min.js +6 -6
  43. package/assets/js/components/card/card.component.min.js.map +1 -1
  44. package/assets/js/components/carousel/carousel.component.min.js +1 -1
  45. package/assets/js/components/collapsible-side/collapsible-side.component.min.js +1 -1
  46. package/assets/js/components/config/config.component.min.js +1 -1
  47. package/assets/js/components/content/content.component.min.js +1 -1
  48. package/assets/js/components/darkmode/darkmode.component.min.js +1 -1
  49. package/assets/js/components/doughnutchart/doughnutchart.component.min.js +1 -1
  50. package/assets/js/components/fileupload/fileupload.component.min.js +1 -1
  51. package/assets/js/components/filter-card/filter-card.component.min.js +2 -2
  52. package/assets/js/components/filter-card/filter-card.component.min.js.map +1 -1
  53. package/assets/js/components/filterlist/filterlist.component.min.js +1 -1
  54. package/assets/js/components/form/form.component.js +42 -151
  55. package/assets/js/components/form/form.component.min.js +3 -3
  56. package/assets/js/components/form/form.component.min.js.map +1 -1
  57. package/assets/js/components/header/header.component.min.js +1 -1
  58. package/assets/js/components/inline-edit/inline-edit.component.min.js +1 -1
  59. package/assets/js/components/input/input.component.min.js +1 -1
  60. package/assets/js/components/input-range/input-range.component.min.js +1 -1
  61. package/assets/js/components/marketing/marketing.component.min.js +1 -1
  62. package/assets/js/components/menu/menu.component.min.js +1 -1
  63. package/assets/js/components/milestone/milestone.component.min.js +1 -1
  64. package/assets/js/components/milestone-group/milestone-group.component.min.js +1 -1
  65. package/assets/js/components/modal/modal.component.js +16 -11
  66. package/assets/js/components/modal/modal.component.min.js +7 -7
  67. package/assets/js/components/modal/modal.component.min.js.map +1 -1
  68. package/assets/js/components/multi-step/multi-step.component.min.js +1 -1
  69. package/assets/js/components/multi-step-modal/multi-step-modal.component.min.js +4 -4
  70. package/assets/js/components/multiselect/multiselect.component.min.js +1 -1
  71. package/assets/js/components/nav/nav.component.js +88 -79
  72. package/assets/js/components/nav/nav.component.min.js +8 -8
  73. package/assets/js/components/nav/nav.component.min.js.map +1 -1
  74. package/assets/js/components/notification/notification.component.min.js +2 -2
  75. package/assets/js/components/pagination/pagination.component.min.js +1 -1
  76. package/assets/js/components/password/password.component.min.js +1 -1
  77. package/assets/js/components/popover/popover.component.min.js +1 -1
  78. package/assets/js/components/rank/rank.component.min.js +1 -1
  79. package/assets/js/components/rankings/rankings.component.min.js +1 -1
  80. package/assets/js/components/rating/rating.component.min.js +1 -1
  81. package/assets/js/components/record-card/record-card.component.min.js +2 -2
  82. package/assets/js/components/record-card/record-card.component.min.js.map +1 -1
  83. package/assets/js/components/search/search.component.js +2 -1
  84. package/assets/js/components/search/search.component.min.js +3 -3
  85. package/assets/js/components/search/search.component.min.js.map +1 -1
  86. package/assets/js/components/skeleton/skeleton.component.min.js +1 -1
  87. package/assets/js/components/slider/slider.component.min.js +1 -1
  88. package/assets/js/components/split-button/split-button.component.min.js +1 -1
  89. package/assets/js/components/std-address-lookup/std-address-lookup.component.min.js +1 -1
  90. package/assets/js/components/std-nav/std-nav.component.js +2 -0
  91. package/assets/js/components/std-nav/std-nav.component.min.js +12 -12
  92. package/assets/js/components/std-nav/std-nav.component.min.js.map +1 -1
  93. package/assets/js/components/std-nav-standalone/std-nav-standalone.component.min.js +2 -2
  94. package/assets/js/components/table/table.component.min.js +1 -1
  95. package/assets/js/components/table-ajax/table-ajax.component.min.js +1 -1
  96. package/assets/js/components/table-basic/table-basic.component.min.js +1 -1
  97. package/assets/js/components/table-no-submit/table-no-submit.component.min.js +1 -1
  98. package/assets/js/components/table-submit/table-submit.component.min.js +1 -1
  99. package/assets/js/components/tabs/tabs.component.min.js +1 -1
  100. package/assets/js/components/tag/tag.component.min.js +1 -1
  101. package/assets/js/components/tooltip/tooltip.component.min.js +1 -1
  102. package/assets/js/components/video/video.component.min.js +1 -1
  103. package/assets/js/components/video-card/video-card.component.min.js +6 -6
  104. package/assets/js/components/video-card/video-card.component.min.js.map +1 -1
  105. package/assets/js/components/video-modal/video-modal.component.min.js +5 -5
  106. package/assets/js/components/word-count/word-count.component.min.js +1 -1
  107. package/assets/js/modules/card.module.js +1 -1
  108. package/assets/js/modules/form.js +129 -0
  109. package/assets/js/modules/form.test.js +132 -0
  110. package/assets/js/modules/test-dom.js +5 -0
  111. package/assets/js/scripts.bundle.js +1 -1
  112. package/assets/js/scripts.bundle.min.js +1 -1
  113. package/assets/sass/_utilities.scss +1 -0
  114. package/assets/sass/components/carousel.config.scss +5 -0
  115. package/assets/sass/components/modal.component.scss +5 -1
  116. package/assets/sass/components/nav.global.scss +0 -10
  117. package/assets/sass/components/notification.global.scss +8 -0
  118. package/assets/sass/elements/dialog.scss +43 -0
  119. package/assets/sass/foundations/colours.scss +0 -24
  120. package/assets/sass/foundations/reboot.scss +4 -0
  121. package/assets/sass/utilities/wordpress.css +7 -0
  122. package/assets/ts/components/form/form.component.ts +54 -213
  123. package/assets/ts/components/modal/modal.component.ts +27 -19
  124. package/assets/ts/components/nav/nav.component.ts +107 -95
  125. package/assets/ts/components/search/search.component.ts +5 -1
  126. package/assets/ts/components/std-nav/std-nav.component.ts +3 -1
  127. package/assets/ts/modules/card.module.ts +1 -1
  128. package/assets/ts/modules/form.test.ts +183 -0
  129. package/assets/ts/modules/form.ts +210 -0
  130. package/assets/ts/modules/test-dom.ts +5 -0
  131. package/dist/components.es.js +24 -24
  132. package/dist/components.umd.js +143 -143
  133. package/package.json +1 -1
@@ -32,7 +32,7 @@ class iamNav extends HTMLElement {
32
32
 
33
33
  <div class="menu__outer">
34
34
  <div class="menu closed">
35
-
35
+
36
36
  <div class="menu__primary">
37
37
  <slot></slot>
38
38
  <slot name="dual"></slot>
@@ -46,7 +46,7 @@ class iamNav extends HTMLElement {
46
46
  </div>
47
47
  </div>
48
48
  <slot name="menus"></slot>
49
- </div>
49
+ </div>
50
50
  </div>
51
51
  <div class="backdrop" part="backdrop"></div>
52
52
  `;
@@ -73,107 +73,119 @@ class iamNav extends HTMLElement {
73
73
  const buttonsHolder = this.shadowRoot.querySelector('.buttons-holder');
74
74
 
75
75
  // Check the content
76
- this.querySelectorAll(':scope > *').forEach(function (element) {
77
- const tagname = element.tagName;
78
-
79
- switch (tagname) {
80
- case 'BUTTON':
81
- if (!element.hasAttribute('slot')) {
82
- element.setAttribute('slot', 'actions');
83
- menu.classList.add('has-actions');
84
- }
85
- break;
86
- }
76
+ const createNavMenu = (component) => {
87
77
 
88
-
89
- // Create menu button
90
- if (
91
- element.classList.contains('nav--menu') &&
92
- element.hasAttribute('data-title') &&
93
- element.hasAttribute('data-icon')
94
- ) {
95
- const title = element.getAttribute('data-title');
96
- const iconClass = element.getAttribute('data-icon');
97
-
98
- // Create the menu button that sits seperately to the menu
99
- const button = document.createElement('button');
100
- button.setAttribute('slot', title);
101
- button.classList.add('btn-menu');
102
- button.setAttribute('part', 'btn-menu');
103
- button.innerHTML = `<span class="btn btn-primary"><span>${title}</span><i class="${iconClass}"></i><i class="fa-regular fa-xmark-large"></i></span>`;
104
- buttonsHolder.insertAdjacentElement('beforeend', button);
105
-
106
- const mdButton = button.querySelector('.btn-primary');
107
-
108
- // Make sure the menu is added to the right part of the component
109
- element.setAttribute('slot', 'menus');
110
-
111
- // If open we need to make sure the main mobile menu is closed, the new button has the right state and the backdrop is shown
112
- if (element.classList.contains('open')) {
113
- button.setAttribute('aria-expanded', true);
114
- mdButton.classList.toggle('active');
115
- iamNav.classList.add('open');
116
- backdrop.classList.add('show');
117
- } else {
118
- element.classList.add('closed'); // closed class is added to prevent the elements being tabbed into, this causes visual issues
78
+ buttonsHolder?.innerHTML = '';
79
+ component.querySelectorAll(':scope > *').forEach(function (element) {
80
+ const tagname = element.tagName;
81
+
82
+ switch (tagname) {
83
+ case 'BUTTON':
84
+ if (!element.hasAttribute('slot')) {
85
+ element.setAttribute('slot', 'actions');
86
+ menu.classList.add('has-actions');
87
+ }
88
+ break;
119
89
  }
120
90
 
121
- // Click event
122
- button.addEventListener(
123
- 'click',
124
- function (e) {
125
- e.preventDefault();
126
- button.toggleAttribute('aria-expanded');
127
- element.classList.toggle('open');
128
- mdButton.classList.toggle('active');
129
91
 
130
- // Close desktop menus
131
- const openMenu = iamNav.querySelector(':scope > details[open]');
132
-
133
- if (openMenu) openMenu.removeAttribute('open');
134
-
135
- // Close the main menu and fix states on the button, iamNav component and backdrop
136
- if (element.classList.contains('open')) {
137
- menu.classList.remove('open');
138
- menuButton.removeAttribute('aria-expanded');
139
- setTimeout(function () {
140
- menu.classList.add('closed');
141
- }, 1000); // Delay until its close so the animation is broken
142
- iamNav.classList.add('open');
143
- backdrop.classList.add('show');
144
- element.classList.remove('closed');
145
- } else {
146
- iamNav.classList.remove('open');
147
- backdrop.classList.remove('show');
148
- setTimeout(function () {
149
- element.classList.add('closed');
150
- }, 1000);
151
- }
92
+ // Create menu button
93
+ if (
94
+ element.classList.contains('nav--menu') &&
95
+ element.hasAttribute('data-title') &&
96
+ element.hasAttribute('data-icon')
97
+ ) {
98
+ const title = element.getAttribute('data-title');
99
+ const iconClass = element.getAttribute('data-icon');
100
+
101
+ // Create the menu button that sits seperately to the menu
102
+ const button = document.createElement('button');
103
+ button.setAttribute('slot', title);
104
+ button.classList.add('btn-menu');
105
+ button.setAttribute('part', 'btn-menu');
106
+ button.innerHTML = `<span class="btn btn-primary"><span>${title}</span><i class="${iconClass}"></i><i class="fa-regular fa-xmark-large"></i></span>`;
107
+ buttonsHolder.insertAdjacentElement('beforeend', button);
152
108
 
153
- // Close any open menus
154
- iamNav.querySelectorAll('.nav--menu.open').forEach(function (openmenu) {
155
- if (openmenu != element) {
156
- openmenu.classList.remove('open');
109
+ const mdButton = button.querySelector('.btn-primary');
110
+
111
+ // Make sure the menu is added to the right part of the component
112
+ element.setAttribute('slot', 'menus');
113
+
114
+ // If open we need to make sure the main mobile menu is closed, the new button has the right state and the backdrop is shown
115
+ if (element.classList.contains('open')) {
116
+ button.setAttribute('aria-expanded', true);
117
+ mdButton.classList.toggle('active');
118
+ component.classList.add('open');
119
+ backdrop.classList.add('show');
120
+ } else {
121
+ element.classList.add('closed'); // closed class is added to prevent the elements being tabbed into, this causes visual issues
122
+ }
123
+
124
+ // Click event
125
+ button.addEventListener(
126
+ 'click',
127
+ function (e) {
128
+ e.preventDefault();
129
+ button.toggleAttribute('aria-expanded');
130
+
131
+ console.log(element);
132
+ element.classList.toggle('open');
133
+ mdButton.classList.toggle('active');
134
+
135
+ // Close desktop menus
136
+ const openMenu = component.querySelector(':scope > details[open]');
137
+
138
+ if (openMenu) openMenu.removeAttribute('open');
139
+
140
+ // Close the main menu and fix states on the button, iamNav component and backdrop
141
+ if (element.classList.contains('open')) {
142
+ menu.classList.remove('open');
143
+ menuButton.removeAttribute('aria-expanded');
144
+ setTimeout(function () {
145
+ menu.classList.add('closed');
146
+ }, 1000); // Delay until its close so the animation is broken
147
+ component.classList.add('open');
148
+ backdrop.classList.add('show');
149
+ element.classList.remove('closed');
150
+ } else {
151
+ component.classList.remove('open');
152
+ backdrop.classList.remove('show');
153
+ setTimeout(function () {
154
+ element.classList.add('closed');
155
+ }, 1000);
157
156
  }
158
- });
159
-
160
- iamNav.shadowRoot
161
- .querySelectorAll('.buttons-holder .btn-menu[aria-expanded]')
162
- .forEach(function (selectedButton) {
163
- if (selectedButton != button) {
164
- selectedButton.removeAttribute('aria-expanded');
165
- const innerBtn = selectedButton.querySelector('.btn-primary');
166
- innerBtn.classList.remove('active');
157
+
158
+ // Close any open menus
159
+ component.querySelectorAll('.nav--menu.open').forEach(function (openmenu) {
160
+ if (openmenu != element) {
161
+ openmenu.classList.remove('open');
167
162
  }
168
163
  });
169
- },
170
- false
171
- );
172
- }
173
- });
174
164
 
175
- this.querySelectorAll('details').forEach(function (element) {
176
- element.classList.add('details--revert');
165
+ component.shadowRoot
166
+ .querySelectorAll('.buttons-holder .btn-menu[aria-expanded]')
167
+ .forEach(function (selectedButton) {
168
+ if (selectedButton != button) {
169
+ selectedButton.removeAttribute('aria-expanded');
170
+ const innerBtn = selectedButton.querySelector('.btn-primary');
171
+ innerBtn.classList.remove('active');
172
+ }
173
+ });
174
+ },
175
+ false
176
+ );
177
+ }
178
+ });
179
+
180
+ component.querySelectorAll('details').forEach(function (element) {
181
+ element.classList.add('details--revert');
182
+ });
183
+ }
184
+ createNavMenu(this);
185
+
186
+ this.addEventListener('rebuilt', () => {
187
+ console.log('rebuilt nav');
188
+ createNavMenu(this);
177
189
  });
178
190
 
179
191
  // Has secondary link
@@ -186,7 +198,7 @@ class iamNav extends HTMLElement {
186
198
  'click',
187
199
  function (e) {
188
200
  e.preventDefault();
189
- menuButton.toggleAttribute('aria-expanded');
201
+ //menuButton.toggleAttribute('aria-expanded');
190
202
  menu.classList.toggle('open');
191
203
 
192
204
  // Close any other menus
@@ -37,7 +37,7 @@ class iamSearch extends HTMLElement {
37
37
  <slot></slot>
38
38
  <button class="clear-search btn btn-action" type="button"><i class="fa-light fa-times me-0"></i></button>
39
39
  </span>
40
- <button class="suffix ${this.getAttribute('data-icon') || 'fa-regular fa-search'}"></button>
40
+ <button class="suffix fa-regular fa-search"></button>
41
41
  </span>
42
42
  <slot name="datalist"></slot>
43
43
  `;
@@ -56,6 +56,10 @@ class iamSearch extends HTMLElement {
56
56
 
57
57
  let minLength = this.hasAttribute('data-min-length') ? getIntegerAttribute(this, 'data-min-length', 1) : 1;
58
58
 
59
+
60
+ suffixElement?.setAttribute('class',`suffix ${this.hasAttribute('data-icon') ? this.getAttribute('data-icon') : 'fa-regular fa-search'}`);
61
+
62
+
59
63
  if (this.hasAttribute('data-url') && !this.hasAttribute('data-min-length')) {
60
64
 
61
65
  minLength = 3;
@@ -36,6 +36,9 @@ class iamSTDNav extends HTMLElement {
36
36
  const defaultContent = navElement.innerHTML;
37
37
 
38
38
  navElement.innerHTML = `${defaultContent}${populateNav(data)}`;
39
+
40
+ const customEvent = new CustomEvent('rebuilt');
41
+ navElement.dispatchEvent(customEvent);
39
42
  }
40
43
 
41
44
  defaultToSecondary = (): void => {
@@ -82,7 +85,6 @@ class iamSTDNav extends HTMLElement {
82
85
  }
83
86
 
84
87
 
85
-
86
88
  const data = await loadNavData(Cookies).then(
87
89
  (data) => {
88
90
 
@@ -38,7 +38,7 @@ export const setupCard = (cardComponent: HTMLElement): void => {
38
38
  const cardTotal = cardBody?.querySelector<HTMLDivElement>('.card__total');
39
39
 
40
40
  if(!cardTotal)
41
- cardBody?.insertAdjacentHTML(
41
+ cardBody.insertAdjacentHTML(
42
42
  'beforeend',
43
43
  `<div class="card__total">${cardComponent.getAttribute('data-total') || ''}</div>`
44
44
  );
@@ -0,0 +1,183 @@
1
+ import { describe, expect, it } from './test.ts';
2
+ import { createElement, installTestDom } from './test-dom.ts';
3
+ import { append } from './test-utils.ts';
4
+ import {
5
+ checkConditions,
6
+ disabledIf,
7
+ emptyIf,
8
+ enabledIf,
9
+ getCheckboxLimit,
10
+ hideIf,
11
+ isFormValid,
12
+ limitCheckboxes,
13
+ readonlyIf,
14
+ requiredIf,
15
+ showIf,
16
+ writeIf,
17
+ } from './form.ts';
18
+
19
+ const { window } = installTestDom();
20
+
21
+ if (typeof globalThis.CustomEvent === 'undefined') {
22
+ globalThis.CustomEvent = class extends Event {
23
+ detail;
24
+
25
+ constructor(type, options = {}) {
26
+ super(type, options);
27
+ this.detail = options.detail;
28
+ }
29
+ };
30
+ }
31
+
32
+ const condition = (id, equals) => JSON.stringify([{ if: id, equals }]);
33
+
34
+ const createChangeEvent = (target) => {
35
+ const event = new Event('change', { bubbles: true, cancelable: true });
36
+ Object.defineProperty(event, 'target', { value: target });
37
+
38
+ return event;
39
+ };
40
+
41
+ describe('Form module', () => {
42
+ it('checks form validity against invalid fields, weak passwords and required multiselects', () => {
43
+ const form = createElement('form');
44
+ const originalQuerySelector = form.querySelector.bind(form);
45
+
46
+ form.querySelector = (selector) => (selector === ':invalid' ? createElement('input') : originalQuerySelector(selector));
47
+
48
+ expect(!isFormValid(form));
49
+
50
+ form.querySelector = originalQuerySelector;
51
+ append(form, createElement('div', { class: 'pwd-checker', dataStrength: '1' }));
52
+
53
+ expect(!isFormValid(form));
54
+
55
+ form.children = [];
56
+ append(form, createElement('iam-multiselect', { dataError: 'true', dataIsRequired: 'true' }));
57
+
58
+ expect(!isFormValid(form));
59
+
60
+ form.children = [];
61
+
62
+ expect(isFormValid(form));
63
+ });
64
+
65
+ it('evaluates JSON conditions against form control values', () => {
66
+ const form = createElement('form');
67
+ append(form, createElement('input', { id: 'status', value: 'active' }));
68
+
69
+ expect(checkConditions(condition('status', 'active'), form));
70
+ expect(!checkConditions(condition('status', 'archived'), form));
71
+ });
72
+
73
+ it('applies visibility conditional helpers', () => {
74
+ const form = createElement('form');
75
+ const status = createElement('input', { id: 'status', value: 'active' });
76
+ const shown = createElement('div', { dataShowIf: condition('status', 'active') });
77
+ const hidden = createElement('div', { dataHideIf: condition('status', 'active') });
78
+ append(form, status, shown, hidden);
79
+
80
+ showIf(form);
81
+ hideIf(form);
82
+
83
+ expect(!shown.classList.contains('d-none'));
84
+ expect(hidden.classList.contains('d-none'));
85
+
86
+ status.value = 'archived';
87
+ showIf(form);
88
+ hideIf(form);
89
+
90
+ expect(shown.classList.contains('d-none'));
91
+ expect(!hidden.classList.contains('d-none'));
92
+ });
93
+
94
+ it('applies enabled, disabled, required and readonly conditional helpers', () => {
95
+ const form = createElement('form');
96
+ const status = createElement('input', { id: 'status', value: 'active' });
97
+ const disabled = createElement('input', { dataDisabledIf: condition('status', 'active') });
98
+ const enabled = createElement('input', { dataEnabledIf: condition('status', 'active') });
99
+ const required = createElement('input', { dataRequiredIf: condition('status', 'active') });
100
+ const readonly = createElement('input', { dataReadonlyIf: condition('status', 'active') });
101
+ const writable = createElement('input', { dataWriteIf: condition('status', 'active') });
102
+ append(form, status, disabled, enabled, required, readonly, writable);
103
+
104
+ disabledIf(form);
105
+ enabledIf(form);
106
+ requiredIf(form);
107
+ readonlyIf(form);
108
+ writeIf(form);
109
+
110
+ expect(disabled.hasAttribute('disabled'));
111
+ expect(!enabled.hasAttribute('disabled'));
112
+ expect(required.hasAttribute('required'));
113
+ expect(readonly.hasAttribute('readonly'));
114
+ expect(!writable.hasAttribute('readonly'));
115
+
116
+ status.value = 'archived';
117
+ disabledIf(form);
118
+ enabledIf(form);
119
+ requiredIf(form);
120
+ readonlyIf(form);
121
+ writeIf(form);
122
+
123
+ expect(!disabled.hasAttribute('disabled'));
124
+ expect(enabled.hasAttribute('disabled'));
125
+ expect(!required.hasAttribute('required'));
126
+ expect(!readonly.hasAttribute('readonly'));
127
+ expect(writable.hasAttribute('readonly'));
128
+ });
129
+
130
+ it('empties form controls when conditions match', () => {
131
+ const form = createElement('form');
132
+ const status = createElement('input', { id: 'status', value: 'clear' });
133
+ const target = createElement('input', { dataEmptyIf: condition('status', 'clear'), value: 'remove me' });
134
+ append(form, status, target);
135
+
136
+ emptyIf(form);
137
+
138
+ expect(target.value === '');
139
+ });
140
+
141
+ it('resolves checkbox limits with a valid positive fallback', () => {
142
+ expect(getCheckboxLimit(createElement('div', { dataCheckboxLimit: '2' })) === 2);
143
+ expect(getCheckboxLimit(createElement('div', { dataCheckboxLimit: '0' })) === 10);
144
+ expect(getCheckboxLimit(createElement('div', { dataCheckboxLimit: 'nope' })) === 10);
145
+ });
146
+
147
+ it('disables unchecked checkboxes and emits analytics when a limit is reached', () => {
148
+ window.dataLayer = [];
149
+ const form = createElement('form');
150
+ const group = createElement('fieldset', { dataCheckboxLimit: '2', id: 'topics' });
151
+ const first = createElement('input', { checked: true, type: 'checkbox' });
152
+ const second = createElement('input', { checked: true, type: 'checkbox' });
153
+ const third = createElement('input', { type: 'checkbox' });
154
+ let eventDetail;
155
+ form.addEventListener('checkbox-limit-reached', (event) => {
156
+ eventDetail = event.detail;
157
+ });
158
+ append(group, first, second, third);
159
+ append(form, group);
160
+
161
+ limitCheckboxes(createChangeEvent(second), form);
162
+
163
+ expect(third.hasAttribute('disabled'));
164
+ expect(eventDetail.element === '#topics');
165
+ expect(eventDetail.limit === 2);
166
+ expect(window.dataLayer[0].event === 'checkbox-limit-reached');
167
+ expect(window.dataLayer[0].element === '#topics');
168
+ });
169
+
170
+ it('prevents checking beyond the configured checkbox limit', () => {
171
+ const form = createElement('form');
172
+ const group = createElement('fieldset', { dataCheckboxLimit: '1' });
173
+ const first = createElement('input', { checked: true, type: 'checkbox' });
174
+ const second = createElement('input', { checked: true, type: 'checkbox' });
175
+ append(group, first, second);
176
+ append(form, group);
177
+
178
+ limitCheckboxes(createChangeEvent(second), form);
179
+
180
+ expect(first.checked);
181
+ expect(!second.checked);
182
+ });
183
+ });
@@ -0,0 +1,210 @@
1
+ type FormControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
2
+
3
+ interface FormCondition {
4
+ equals: string | number | boolean | null;
5
+ if: string;
6
+ }
7
+
8
+ interface CheckboxLimitDetail {
9
+ element: string;
10
+ limit: number;
11
+ }
12
+
13
+ const getConditions = (conditions: string | null): FormCondition[] => JSON.parse(conditions || '[]') as FormCondition[];
14
+
15
+ const getFormControl = (form: HTMLElement, id: string): FormControlElement | null =>
16
+ form.querySelector<FormControlElement>(`#${id}`);
17
+
18
+ export const isFormValid = (form: HTMLFormElement): boolean => {
19
+
20
+ if (form.querySelector(':invalid'))
21
+ return false;
22
+
23
+ if (form.querySelector('.pwd-checker[data-strength="1"]') || form.querySelector('.pwd-checker[data-strength="2"]'))
24
+ return false;
25
+
26
+ if (form.querySelector('iam-multiselect[data-is-required][data-error]'))
27
+ return false;
28
+
29
+ return true;
30
+ };
31
+
32
+ export const checkConditions = (conditions: string | null, form: HTMLElement): boolean => {
33
+
34
+ let meetsCondition = true;
35
+
36
+ getConditions(conditions).forEach((condition) => {
37
+ const input = getFormControl(form, condition.if);
38
+
39
+ if(input?.value != condition.equals)
40
+ meetsCondition = false;
41
+ });
42
+
43
+ return meetsCondition;
44
+ }
45
+
46
+
47
+ export const showIf = (form: HTMLElement): void => {
48
+
49
+ form.querySelectorAll<HTMLElement>('[data-show-if]').forEach((element) => {
50
+
51
+ if(!checkConditions(element.getAttribute('data-show-if'), form))
52
+ element.classList.add('d-none');
53
+ else
54
+ element.classList.remove('d-none');
55
+
56
+ });
57
+ }
58
+
59
+ export const hideIf = (form: HTMLElement): void => {
60
+
61
+ form.querySelectorAll<HTMLElement>('[data-hide-if]').forEach((element) => {
62
+
63
+ if(checkConditions(element.getAttribute('data-hide-if'), form))
64
+ element.classList.add('d-none');
65
+ else
66
+ element.classList.remove('d-none');
67
+
68
+ });
69
+ }
70
+
71
+ export const disabledIf = (form: HTMLElement): void => {
72
+
73
+ form.querySelectorAll<HTMLElement>('[data-disabled-if]').forEach((element) => {
74
+
75
+ if(checkConditions(element.getAttribute('data-disabled-if'), form))
76
+ element.setAttribute('disabled','disabled');
77
+ else
78
+ element.removeAttribute('disabled');
79
+
80
+ });
81
+ }
82
+
83
+ export const enabledIf = (form: HTMLElement): void => {
84
+
85
+ form.querySelectorAll<HTMLElement>('[data-enabled-if]').forEach((element) => {
86
+
87
+ if(!checkConditions(element.getAttribute('data-enabled-if'), form))
88
+ element.setAttribute('disabled','disabled');
89
+ else
90
+ element.removeAttribute('disabled');
91
+
92
+ });
93
+ }
94
+
95
+ export const requiredIf = (form: HTMLElement): void => {
96
+
97
+ form.querySelectorAll<HTMLElement>('[data-required-if]').forEach((element) => {
98
+
99
+ if(checkConditions(element.getAttribute('data-required-if'), form))
100
+ element.setAttribute('required','required');
101
+ else
102
+ element.removeAttribute('required');
103
+
104
+ });
105
+ }
106
+
107
+ export const readonlyIf = (form: HTMLElement): void => {
108
+
109
+ form.querySelectorAll<HTMLElement>('[data-readonly-if]').forEach((element) => {
110
+
111
+ if(checkConditions(element.getAttribute('data-readonly-if'), form))
112
+ element.setAttribute('readonly','readonly');
113
+ else
114
+ element.removeAttribute('readonly');
115
+
116
+ });
117
+ }
118
+
119
+ export const writeIf = (form: HTMLElement): void => {
120
+
121
+ form.querySelectorAll<HTMLElement>('[data-write-if]').forEach((element) => {
122
+
123
+ if(!checkConditions(element.getAttribute('data-write-if'), form))
124
+ element.setAttribute('readonly','readonly');
125
+ else
126
+ element.removeAttribute('readonly');
127
+
128
+ });
129
+ }
130
+
131
+ export const emptyIf = (form: HTMLElement): void => {
132
+
133
+ form.querySelectorAll<FormControlElement>('[data-empty-if]').forEach((element) => {
134
+
135
+ if(checkConditions(element.getAttribute('data-empty-if'), form))
136
+ element.value = "";
137
+
138
+ });
139
+ }
140
+
141
+ export const getCheckboxLimit = (element: HTMLElement): number => {
142
+
143
+ const limit = parseInt(element.getAttribute('data-checkbox-limit') || '10', 10);
144
+
145
+ return !isNaN(limit) && limit > 0 ? limit : 10;
146
+ }
147
+
148
+ export const limitCheckboxes = (event: Event | null | undefined, root: HTMLElement): void => {
149
+
150
+ console.log(event);
151
+
152
+
153
+ const target = event?.target instanceof HTMLInputElement ? event.target : null;
154
+ const changedCheckbox = target?.matches('input[type="checkbox"]') ? target : null;
155
+ const checkboxLimitGroup = changedCheckbox?.closest<HTMLElement>('[data-checkbox-limit]');
156
+ const checkboxLimitGroups = checkboxLimitGroup
157
+ ? [checkboxLimitGroup]
158
+ : [
159
+ ...(root.hasAttribute('data-checkbox-limit') ? [root] : []),
160
+ ...Array.from(root.querySelectorAll<HTMLElement>('[data-checkbox-limit]')),
161
+ ];
162
+
163
+ checkboxLimitGroups.forEach((group) => {
164
+
165
+ const limit = getCheckboxLimit(group);
166
+ const checked = Array.from(group.querySelectorAll<HTMLInputElement>('input[type="checkbox"]:checked'));
167
+ const notChecked = Array.from(group.querySelectorAll<HTMLInputElement>('input[type="checkbox"]:not(:checked)'));
168
+
169
+ notChecked.forEach((checkbox) => {
170
+
171
+ checkbox.setAttribute('disabled','disabled');
172
+ });
173
+
174
+ if(checked.length < limit){
175
+ notChecked.forEach((checkbox) => {
176
+
177
+ checkbox.removeAttribute('disabled');
178
+ });
179
+
180
+ return;
181
+ }
182
+
183
+ if(checked.length == limit){
184
+
185
+ // Data layer Web component created
186
+
187
+ const eventDetails: CheckboxLimitDetail = {element: group.hasAttribute('id') ? `#${group.getAttribute('id')}` :'', limit: limit};
188
+ const changeEvent = new CustomEvent<CheckboxLimitDetail>('checkbox-limit-reached', { detail: eventDetails });
189
+
190
+ root.dispatchEvent(changeEvent);
191
+
192
+ const formWindow = window as WindowWithDataLayer;
193
+ formWindow.dataLayer = formWindow.dataLayer || [];
194
+ formWindow.dataLayer.push({'event': 'checkbox-limit-reached', ...eventDetails});
195
+
196
+ return;
197
+ }
198
+
199
+
200
+ if(changedCheckbox?.checked && group.contains(changedCheckbox)) {
201
+ changedCheckbox.checked = false;
202
+ return;
203
+ }
204
+
205
+ checked.slice(limit).forEach((checkbox) => {
206
+
207
+ checkbox.checked = false;
208
+ });
209
+ });
210
+ }