@iamproperty/components 7.8.2--beta3 → 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 (235) 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/address-lookup.component.css +1 -1
  4. package/assets/css/components/address-lookup.component.css.map +1 -1
  5. package/assets/css/components/advanced-select.component.css +1 -1
  6. package/assets/css/components/advanced-select.component.css.map +1 -1
  7. package/assets/css/components/banner.preload.css +1 -0
  8. package/assets/css/components/banner.preload.css.map +1 -0
  9. package/assets/css/components/calendar.component.css +1 -1
  10. package/assets/css/components/calendar.component.css.map +1 -1
  11. package/assets/css/components/card.component.css +1 -1
  12. package/assets/css/components/card.component.css.map +1 -1
  13. package/assets/css/components/card.module.css +1 -1
  14. package/assets/css/components/card.module.css.map +1 -1
  15. package/assets/css/components/card.preload.css +1 -0
  16. package/assets/css/components/card.preload.css.map +1 -0
  17. package/assets/css/components/carousel.component.css +1 -1
  18. package/assets/css/components/carousel.component.css.map +1 -1
  19. package/assets/css/components/carousel.config.css +1 -1
  20. package/assets/css/components/carousel.config.css.map +1 -1
  21. package/assets/css/components/config.component.css +1 -1
  22. package/assets/css/components/config.component.css.map +1 -1
  23. package/assets/css/components/content.component.css +1 -1
  24. package/assets/css/components/content.component.css.map +1 -1
  25. package/assets/css/components/fileupload.css +1 -1
  26. package/assets/css/components/fileupload.css.map +1 -1
  27. package/assets/css/components/filter-card.component.css +1 -1
  28. package/assets/css/components/filter-card.component.css.map +1 -1
  29. package/assets/css/components/modal.component.css +1 -1
  30. package/assets/css/components/modal.component.css.map +1 -1
  31. package/assets/css/components/multi-step-modal.component.css +1 -1
  32. package/assets/css/components/multi-step-modal.component.css.map +1 -1
  33. package/assets/css/components/multiselect.css +1 -1
  34. package/assets/css/components/multiselect.css.map +1 -1
  35. package/assets/css/components/nav.component.css +1 -1
  36. package/assets/css/components/nav.component.css.map +1 -1
  37. package/assets/css/components/nav.global.css +1 -1
  38. package/assets/css/components/nav.global.css.map +1 -1
  39. package/assets/css/components/notification.global.css +1 -1
  40. package/assets/css/components/notification.global.css.map +1 -1
  41. package/assets/css/components/pagination.css +1 -1
  42. package/assets/css/components/pagination.css.map +1 -1
  43. package/assets/css/components/record-card.component.css +1 -1
  44. package/assets/css/components/record-card.component.css.map +1 -1
  45. package/assets/css/components/search.component.css +1 -1
  46. package/assets/css/components/search.component.css.map +1 -1
  47. package/assets/css/components/skeleton.global.css +1 -1
  48. package/assets/css/components/skeleton.global.css.map +1 -1
  49. package/assets/css/components/slider.css +1 -1
  50. package/assets/css/components/slider.css.map +1 -1
  51. package/assets/css/components/split-button.component.css +1 -1
  52. package/assets/css/components/split-button.component.css.map +1 -1
  53. package/assets/css/components/std-nav-standalone.component.css +1 -1
  54. package/assets/css/components/std-nav-standalone.component.css.map +1 -1
  55. package/assets/css/components/tabs.component.css +1 -1
  56. package/assets/css/components/tabs.component.css.map +1 -1
  57. package/assets/css/components/tag.component.css +1 -1
  58. package/assets/css/components/tag.component.css.map +1 -1
  59. package/assets/css/components/video-card.component.css +1 -1
  60. package/assets/css/components/video-card.component.css.map +1 -1
  61. package/assets/css/components/video-modal.component.css +1 -1
  62. package/assets/css/components/video-modal.component.css.map +1 -1
  63. package/assets/css/core.min.css +1 -1
  64. package/assets/css/core.min.css.map +1 -1
  65. package/assets/css/elements/dialog.css +1 -1
  66. package/assets/css/elements/dialog.css.map +1 -1
  67. package/assets/css/elements/dropdown.css +1 -1
  68. package/assets/css/elements/dropdown.css.map +1 -1
  69. package/assets/css/elements/forms.css +1 -1
  70. package/assets/css/elements/forms.css.map +1 -1
  71. package/assets/css/elements/links--global.css +1 -1
  72. package/assets/css/elements/links--global.css.map +1 -1
  73. package/assets/css/elements/links.css +1 -1
  74. package/assets/css/elements/links.css.map +1 -1
  75. package/assets/css/style.min.css +1 -1
  76. package/assets/css/style.min.css.map +1 -1
  77. package/assets/js/components/accordion/accordion.component.min.js +1 -1
  78. package/assets/js/components/actionbar/actionbar.component.min.js +3 -3
  79. package/assets/js/components/address-lookup/address-lookup.component.min.js +4 -4
  80. package/assets/js/components/address-lookup/address-lookup.component.min.js.map +1 -1
  81. package/assets/js/components/advanced-select/advanced-select.component.min.js +2 -2
  82. package/assets/js/components/applied-filters/applied-filters.component.min.js +1 -1
  83. package/assets/js/components/banner/banner.component.min.js +1 -1
  84. package/assets/js/components/barchart/barchart.component.min.js +1 -1
  85. package/assets/js/components/bento-grid/bento-grid.component.min.js +1 -1
  86. package/assets/js/components/bone/bone.component.min.js +1 -1
  87. package/assets/js/components/button/button.component.min.js +1 -1
  88. package/assets/js/components/calendar/calendar.component.min.js +2 -2
  89. package/assets/js/components/card/card.component.js +114 -125
  90. package/assets/js/components/card/card.component.min.js +7 -7
  91. package/assets/js/components/card/card.component.min.js.map +1 -1
  92. package/assets/js/components/carousel/carousel.component.js +83 -29
  93. package/assets/js/components/carousel/carousel.component.min.js +16 -11
  94. package/assets/js/components/carousel/carousel.component.min.js.map +1 -1
  95. package/assets/js/components/collapsible-side/collapsible-side.component.min.js +1 -1
  96. package/assets/js/components/config/config.component.min.js +7 -7
  97. package/assets/js/components/config/config.component.min.js.map +1 -1
  98. package/assets/js/components/content/content.component.js +28 -69
  99. package/assets/js/components/content/content.component.min.js +4 -4
  100. package/assets/js/components/content/content.component.min.js.map +1 -1
  101. package/assets/js/components/darkmode/darkmode.component.min.js +1 -1
  102. package/assets/js/components/doughnutchart/doughnutchart.component.min.js +1 -1
  103. package/assets/js/components/fileupload/fileupload.component.min.js +2 -2
  104. package/assets/js/components/filter-card/filter-card.component.min.js +5 -5
  105. package/assets/js/components/filter-card/filter-card.component.min.js.map +1 -1
  106. package/assets/js/components/filterlist/filterlist.component.min.js +1 -1
  107. package/assets/js/components/form/form.component.js +42 -151
  108. package/assets/js/components/form/form.component.min.js +3 -3
  109. package/assets/js/components/form/form.component.min.js.map +1 -1
  110. package/assets/js/components/header/header.component.min.js +1 -1
  111. package/assets/js/components/inline-edit/inline-edit.component.min.js +1 -1
  112. package/assets/js/components/input/input.component.min.js +1 -1
  113. package/assets/js/components/input-range/input-range.component.min.js +1 -1
  114. package/assets/js/components/marketing/marketing.component.min.js +1 -1
  115. package/assets/js/components/menu/menu.component.min.js +1 -1
  116. package/assets/js/components/milestone/milestone.component.min.js +1 -1
  117. package/assets/js/components/milestone-group/milestone-group.component.min.js +1 -1
  118. package/assets/js/components/modal/modal.component.js +16 -11
  119. package/assets/js/components/modal/modal.component.min.js +7 -7
  120. package/assets/js/components/modal/modal.component.min.js.map +1 -1
  121. package/assets/js/components/multi-step/multi-step.component.min.js +1 -1
  122. package/assets/js/components/multi-step-modal/multi-step-modal.component.min.js +4 -4
  123. package/assets/js/components/multiselect/multiselect.component.min.js +4 -4
  124. package/assets/js/components/multiselect/multiselect.component.min.js.map +1 -1
  125. package/assets/js/components/nav/nav.component.js +88 -79
  126. package/assets/js/components/nav/nav.component.min.js +8 -8
  127. package/assets/js/components/nav/nav.component.min.js.map +1 -1
  128. package/assets/js/components/notification/notification.component.min.js +2 -2
  129. package/assets/js/components/pagination/pagination.component.min.js +5 -5
  130. package/assets/js/components/password/password.component.min.js +1 -1
  131. package/assets/js/components/popover/popover.component.min.js +1 -1
  132. package/assets/js/components/rank/rank.component.min.js +1 -1
  133. package/assets/js/components/rankings/rankings.component.min.js +1 -1
  134. package/assets/js/components/rating/rating.component.min.js +1 -1
  135. package/assets/js/components/record-card/record-card.component.min.js +6 -6
  136. package/assets/js/components/record-card/record-card.component.min.js.map +1 -1
  137. package/assets/js/components/search/search.component.js +235 -186
  138. package/assets/js/components/search/search.component.min.js +12 -7
  139. package/assets/js/components/search/search.component.min.js.map +1 -1
  140. package/assets/js/components/skeleton/skeleton.component.min.js +1 -1
  141. package/assets/js/components/slider/slider.component.min.js +2 -2
  142. package/assets/js/components/split-button/split-button.component.min.js +2 -2
  143. package/assets/js/components/std-address-lookup/std-address-lookup.component.min.js +5 -5
  144. package/assets/js/components/std-address-lookup/std-address-lookup.component.min.js.map +1 -1
  145. package/assets/js/components/std-nav/std-nav.component.js +12 -9
  146. package/assets/js/components/std-nav/std-nav.component.min.js +12 -15
  147. package/assets/js/components/std-nav/std-nav.component.min.js.map +1 -1
  148. package/assets/js/components/std-nav-standalone/std-nav-standalone.component.min.js +5 -5
  149. package/assets/js/components/std-nav-standalone/std-nav-standalone.component.min.js.map +1 -1
  150. package/assets/js/components/table/table.component.min.js +1 -1
  151. package/assets/js/components/table-ajax/table-ajax.component.min.js +1 -1
  152. package/assets/js/components/table-basic/table-basic.component.min.js +1 -1
  153. package/assets/js/components/table-no-submit/table-no-submit.component.min.js +1 -1
  154. package/assets/js/components/table-submit/table-submit.component.min.js +1 -1
  155. package/assets/js/components/tabs/tabs.component.min.js +4 -4
  156. package/assets/js/components/tag/tag.component.min.js +3 -3
  157. package/assets/js/components/tag/tag.component.min.js.map +1 -1
  158. package/assets/js/components/tooltip/tooltip.component.min.js +1 -1
  159. package/assets/js/components/video/video.component.min.js +1 -1
  160. package/assets/js/components/video-card/video-card.component.min.js +9 -9
  161. package/assets/js/components/video-card/video-card.component.min.js.map +1 -1
  162. package/assets/js/components/video-modal/video-modal.component.min.js +5 -5
  163. package/assets/js/components/word-count/word-count.component.min.js +1 -1
  164. package/assets/js/modules/card.module.js +12 -11
  165. package/assets/js/modules/content.js +40 -8
  166. package/assets/js/modules/content.test.js +62 -12
  167. package/assets/js/modules/data-layer.js +7 -6
  168. package/assets/js/modules/dropdown.js +0 -1
  169. package/assets/js/modules/form.js +129 -0
  170. package/assets/js/modules/form.test.js +132 -0
  171. package/assets/js/modules/nav.js +10 -3
  172. package/assets/js/modules/search.js +153 -0
  173. package/assets/js/modules/search.test.js +125 -0
  174. package/assets/js/modules/tabs.test.js +64 -12
  175. package/assets/js/modules/test-dom.js +5 -0
  176. package/assets/js/modules/testimonial.test.js +44 -6
  177. package/assets/js/modules/videos.test.js +61 -13
  178. package/assets/js/scripts.bundle.js +3 -3
  179. package/assets/js/scripts.bundle.js.map +1 -1
  180. package/assets/js/scripts.bundle.min.js +2 -2
  181. package/assets/js/scripts.bundle.min.js.map +1 -1
  182. package/assets/sass/_components.scss +2 -63
  183. package/assets/sass/_utilities.scss +1 -0
  184. package/assets/sass/components/banner.preload.scss +26 -0
  185. package/assets/sass/components/card.component.scss +1 -7
  186. package/assets/sass/components/card.module.scss +6 -6
  187. package/assets/sass/components/card.preload.scss +80 -0
  188. package/assets/sass/components/carousel.component.scss +165 -0
  189. package/assets/sass/components/carousel.config.scss +90 -249
  190. package/assets/sass/components/content.component.scss +0 -7
  191. package/assets/sass/components/modal.component.scss +5 -1
  192. package/assets/sass/components/nav.component.scss +2 -1
  193. package/assets/sass/components/nav.global.scss +0 -10
  194. package/assets/sass/components/notification.global.scss +8 -0
  195. package/assets/sass/components/search.component.scss +89 -7
  196. package/assets/sass/components/skeleton.global.scss +4 -0
  197. package/assets/sass/elements/dialog.scss +43 -0
  198. package/assets/sass/elements/dropdown.css +2 -0
  199. package/assets/sass/elements/forms.scss +0 -27
  200. package/assets/sass/elements/links--global.scss +40 -2
  201. package/assets/sass/foundations/colours.scss +0 -24
  202. package/assets/sass/foundations/reboot.scss +4 -0
  203. package/assets/sass/foundations/root.scss +0 -1
  204. package/assets/sass/utilities/js-display.css +2 -3
  205. package/assets/sass/utilities/wordpress.css +7 -0
  206. package/assets/ts/components/card/card.component.ts +72 -62
  207. package/assets/ts/components/carousel/carousel.component.ts +84 -19
  208. package/assets/ts/components/content/content.component.ts +36 -100
  209. package/assets/ts/components/form/form.component.ts +54 -213
  210. package/assets/ts/components/modal/modal.component.ts +27 -19
  211. package/assets/ts/components/nav/nav.component.ts +107 -95
  212. package/assets/ts/components/search/search.component.ts +260 -184
  213. package/assets/ts/components/std-nav/std-nav.component.ts +20 -17
  214. package/assets/ts/html.d.ts +6 -0
  215. package/assets/ts/modules/card.module.ts +19 -11
  216. package/assets/ts/modules/content.test.ts +84 -12
  217. package/assets/ts/modules/content.ts +56 -9
  218. package/assets/ts/modules/data-layer.ts +7 -11
  219. package/assets/ts/modules/dropdown.ts +0 -2
  220. package/assets/ts/modules/form.test.ts +183 -0
  221. package/assets/ts/modules/form.ts +210 -0
  222. package/assets/ts/modules/nav.ts +12 -3
  223. package/assets/ts/modules/search.test.ts +142 -0
  224. package/assets/ts/modules/search.ts +206 -0
  225. package/assets/ts/modules/tabs.test.ts +79 -12
  226. package/assets/ts/modules/test-dom.ts +5 -0
  227. package/assets/ts/modules/testimonial.test.ts +45 -6
  228. package/assets/ts/modules/videos.test.ts +74 -14
  229. package/dist/components.es.js +25 -25
  230. package/dist/components.umd.js +170 -163
  231. package/package.json +1 -1
  232. package/assets/js/modules/carousel.js +0 -214
  233. package/assets/js/modules/carousel.test.js +0 -18
  234. package/assets/ts/modules/carousel.test.ts +0 -27
  235. package/assets/ts/modules/carousel.ts +0 -301
@@ -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
+ }
@@ -74,8 +74,9 @@ export const populateLinks = (data):void => {
74
74
 
75
75
  export const loadNavData = async(Cookies): any => {
76
76
 
77
- //const ajaxURL = 'https://dev.hub.iamproperty.group/data/ecosystem-switcher.json';
78
- const ajaxURL = '/nav.json';
77
+ const ajaxURL = 'https://dev.hub.iamproperty.group/data/ecosystem-switcher.json';
78
+
79
+ //const ajaxURL = '/nav.json';
79
80
 
80
81
  // Setup controller vars if not already set
81
82
  if (!window.controller) window.controller = [];
@@ -145,7 +146,15 @@ export const loadUserData = async(Cookies): any => {
145
146
 
146
147
  export const setEnabledLinks = (component,data):void => {
147
148
 
148
- component.querySelectorAll(`[data-product][data-feature]`).forEach((element) => {
149
+ const selector = `[data-product][data-feature]`;
150
+ const elements = component
151
+ ? [
152
+ ...component.querySelectorAll(selector),
153
+ ...(component.shadowRoot ? component.shadowRoot.querySelectorAll(selector) : []),
154
+ ]
155
+ : document.querySelectorAll(`iam-nav ${selector}`);
156
+
157
+ elements.forEach((element) => {
149
158
  const isEnabled = data.attributes.products[element.getAttribute('data-product')].features[element.getAttribute('data-feature')];
150
159
  element.setAttribute('data-is-enabled',isEnabled);
151
160
  if(isEnabled && element.getAttribute('data-enabled')){
@@ -0,0 +1,142 @@
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 search, { datalistSelectOption, filterDatalist } from './search.ts';
5
+
6
+ installTestDom();
7
+
8
+ if (typeof globalThis.CustomEvent === 'undefined') {
9
+ globalThis.CustomEvent = class extends Event {
10
+ detail;
11
+
12
+ constructor(type, options = {}) {
13
+ super(type, options);
14
+ this.detail = options.detail;
15
+ }
16
+ };
17
+ }
18
+
19
+ describe('Search module', () => {
20
+ it('fetches GET results, builds datalist options and filters them by the search term', async () => {
21
+ let requestedUrl = '';
22
+ let requestedOptions;
23
+
24
+ globalThis.fetch = (url, options) => {
25
+ requestedUrl = url;
26
+ requestedOptions = options;
27
+
28
+ return Promise.resolve({
29
+ json: () =>
30
+ Promise.resolve({
31
+ data: [
32
+ { value: 'alpha-id', label: 'Alpha\nTeam' },
33
+ { id: 'beta-id', title: 'Beta Team' },
34
+ ],
35
+ }),
36
+ });
37
+ };
38
+
39
+ const component = createElement('iam-search', { dataUrl: '/results' });
40
+ const input = createElement('input', { name: 'q', value: 'alpha & beta' });
41
+ const datalist = createElement('datalist');
42
+ append(component, input, datalist);
43
+
44
+ await search(component, datalist, 'alpha');
45
+
46
+ expect(requestedUrl === '/results?q=alpha%20%26%20beta');
47
+ expect(requestedOptions.method === 'GET');
48
+ expect(datalist.options.length === 2);
49
+ expect(datalist.options[0].value === 'alpha-id');
50
+ expect(datalist.options[0].textContent === 'Alpha, Team');
51
+ expect(!datalist.options[0].classList.contains('js-hide'));
52
+ expect(datalist.options[1].classList.contains('js-hide'));
53
+ });
54
+
55
+ it('posts form values and renders grouped response options with custom schemas', async () => {
56
+ let requestedUrl = '';
57
+ let requestedOptions;
58
+
59
+ globalThis.fetch = (url, options) => {
60
+ requestedUrl = url;
61
+ requestedOptions = options;
62
+
63
+ return Promise.resolve({
64
+ json: () =>
65
+ Promise.resolve({
66
+ results: {
67
+ groups: {
68
+ Products: [{ code: 'eco', name: 'Ecosystem' }],
69
+ Learning: [{ code: 'guide', name: 'Guide' }],
70
+ },
71
+ },
72
+ }),
73
+ });
74
+ };
75
+
76
+ const component = createElement('iam-search', {
77
+ dataDisplaySchema: 'name',
78
+ dataMethod: 'POST',
79
+ dataSchema: 'results.groups',
80
+ dataUrl: '/lookup',
81
+ dataValueSchema: 'code',
82
+ });
83
+ const input = createElement('input', { name: 'market', value: 'auction' });
84
+ const select = createElement('select', { name: 'status', value: 'active' });
85
+ const datalist = createElement('datalist');
86
+ append(component, input, select, datalist);
87
+
88
+ await search(component, datalist, 'guide');
89
+
90
+ expect(requestedUrl === '/lookup');
91
+ expect(requestedOptions.method === 'POST');
92
+ expect(requestedOptions.body === '{"market":"auction","status":"active"}');
93
+ expect(datalist.options.length === 2);
94
+ expect(datalist.options[0].value === 'eco');
95
+ expect(datalist.options[0].textContent === 'Products: Ecosystem');
96
+ expect(datalist.options[0].classList.contains('js-hide'));
97
+ expect(datalist.options[1].textContent === 'Learning: Guide');
98
+ expect(!datalist.options[1].classList.contains('js-hide'));
99
+ });
100
+
101
+ it('filters datalist options using visible text before value', () => {
102
+ const datalist = createElement('datalist');
103
+ const visibleMatch = createElement('option', { value: 'hidden-value' }, 'Matching label');
104
+ const valueMatch = createElement('option', { value: 'value match' });
105
+ const noMatch = createElement('option', { value: 'elsewhere' }, 'Different label');
106
+ append(datalist, visibleMatch, valueMatch, noMatch);
107
+
108
+ filterDatalist(datalist, 'match');
109
+
110
+ expect(!visibleMatch.classList.contains('js-hide'));
111
+ expect(!valueMatch.classList.contains('js-hide'));
112
+ expect(noMatch.classList.contains('js-hide'));
113
+ });
114
+
115
+ it('selects a datalist option, stores alternate values and dispatches selection details', () => {
116
+ const component = createElement('iam-search');
117
+ const input = createElement('input', { name: 'product' });
118
+ const datalist = createElement('datalist');
119
+ const inactiveOption = createElement('option', { value: 'inactive-id' }, 'Inactive');
120
+ const option = createElement('option', { dataUrl: '/products/alpha', value: 'alpha-id' }, 'Alpha');
121
+ let selectedDetail;
122
+
123
+ component.addEventListener('option-selected', (event) => {
124
+ selectedDetail = event.detail;
125
+ });
126
+
127
+ append(datalist, inactiveOption, option);
128
+ append(component, input, datalist);
129
+
130
+ datalistSelectOption(component, input, option);
131
+
132
+ expect(input.value === 'Alpha');
133
+ expect(input.getAttribute('data-value') === 'Alpha');
134
+ expect(input.getAttribute('placeholder') === 'Alpha');
135
+ expect(component.innerHTML.includes('name="productAlt" value="alpha-id"'));
136
+ expect(option.classList.contains('active'));
137
+ expect(!inactiveOption.classList.contains('active'));
138
+ expect(selectedDetail.title === 'Alpha');
139
+ expect(selectedDetail.value === 'alpha-id');
140
+ expect(selectedDetail.url === '/products/alpha');
141
+ });
142
+ });
@@ -0,0 +1,206 @@
1
+ import { resolvePath, isTraversable } from './helpers';
2
+
3
+ type SearchComponent = HTMLElement;
4
+ type SearchFormControl = HTMLInputElement | HTMLSelectElement;
5
+ type SearchResultItem = Record<string, unknown> | string | number | boolean | null | undefined;
6
+ type SearchResponse = Record<string, unknown>;
7
+ type WindowWithControllers = Window & {
8
+ controller?: Record<string, AbortController>;
9
+ };
10
+
11
+ interface OptionSelectedDetail {
12
+ title: string;
13
+ value: string;
14
+ url: string;
15
+ }
16
+
17
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
18
+ value !== null && typeof value === 'object' && !Array.isArray(value);
19
+
20
+ const getResultValue = (item: SearchResultItem, key: string): unknown => (isRecord(item) ? item[key] : undefined);
21
+
22
+ const toOptionText = (value: unknown): string => String(value ?? '').replace('\n', ', ');
23
+
24
+ const appendDatalistOption = (
25
+ datalistElement: HTMLDataListElement,
26
+ item: SearchResultItem,
27
+ valueSchema: string,
28
+ displaySchema: string,
29
+ groupLabel = ''
30
+ ): void => {
31
+ const resolvedValue = resolvePath(item, valueSchema, undefined);
32
+ const resolvedDisplay = resolvePath(item, displaySchema, undefined);
33
+ const fallbackValue = isTraversable(item) ? '' : item;
34
+ const actualValue =
35
+ resolvedValue ??
36
+ getResultValue(item, 'value') ??
37
+ getResultValue(item, 'id') ??
38
+ resolvedDisplay ??
39
+ getResultValue(item, 'title') ??
40
+ getResultValue(item, 'label') ??
41
+ fallbackValue;
42
+ const displayValue = toOptionText(
43
+ resolvedDisplay ?? getResultValue(item, 'title') ?? getResultValue(item, 'label') ?? actualValue
44
+ );
45
+
46
+ if (!displayValue) return;
47
+
48
+ const optionElement = document.createElement('option');
49
+ optionElement.value = String(actualValue);
50
+ optionElement.textContent = `${groupLabel}${displayValue}`;
51
+ datalistElement.appendChild(optionElement);
52
+ };
53
+
54
+ const getFormControls = (component: SearchComponent): SearchFormControl[] =>
55
+ Array.from(component.querySelectorAll<SearchFormControl>('input,select'));
56
+
57
+ const getSearchSchema = (component: SearchComponent, attributeName: string, fallback: string): string =>
58
+ component.hasAttribute(attributeName) ? component.getAttribute(attributeName) || '' : fallback;
59
+
60
+ const search = async (
61
+ component: SearchComponent,
62
+ datalistElement: HTMLDataListElement,
63
+ searchTerm: string
64
+ ): Promise<void> => {
65
+ let url = component.getAttribute('data-url');
66
+
67
+ if (!url) return;
68
+
69
+ const method = component.getAttribute('data-method') || 'GET';
70
+ const body: Record<string, string> = {};
71
+ const searchWindow = window as WindowWithControllers;
72
+
73
+ // Setup controller vars if not already set
74
+ if (!searchWindow.controller) searchWindow.controller = {};
75
+
76
+ // Abort if controller already present for this url
77
+ if (searchWindow.controller[url]) searchWindow.controller[url].abort();
78
+
79
+ // Create a new controller so it can be aborted if new fetch made
80
+ searchWindow.controller[url] = new AbortController();
81
+ const { signal } = searchWindow.controller[url];
82
+
83
+ const requestOptions: RequestInit = {
84
+ signal,
85
+ method,
86
+ headers: new Headers({
87
+ 'Content-Type': 'application/json',
88
+ Accept: 'application/json',
89
+ }),
90
+ };
91
+
92
+ if (method.toUpperCase() === 'GET') {
93
+ getFormControls(component).forEach((input) => {
94
+ const name = input.getAttribute('name');
95
+ const value = input.value;
96
+
97
+ if (name && value) {
98
+ url += `${url.includes('?') ? '&' : '?'}${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
99
+ }
100
+ });
101
+ } else {
102
+ getFormControls(component).forEach((input) => {
103
+ const name = input.getAttribute('name');
104
+ const value = input.value;
105
+
106
+ if (name && value) {
107
+ body[name] = value;
108
+ }
109
+ });
110
+
111
+ requestOptions['body'] = JSON.stringify(body);
112
+ }
113
+
114
+ try {
115
+ const response = await fetch(url, requestOptions);
116
+ const responseData = (await response.json()) as SearchResponse;
117
+ const loopSchema = getSearchSchema(component, 'data-schema', 'data');
118
+ const valueSchema = getSearchSchema(component, 'data-value-schema', 'value');
119
+ const displaySchema = getSearchSchema(component, 'data-display-schema', 'label');
120
+ const loopValues = resolvePath(responseData, loopSchema, []);
121
+
122
+ if (Array.isArray(loopValues)) {
123
+ loopValues.forEach((item: SearchResultItem) => {
124
+ appendDatalistOption(datalistElement, item, valueSchema, displaySchema);
125
+ });
126
+ } else if (isRecord(loopValues)) {
127
+ Object.entries(loopValues).forEach(([key, value]) => {
128
+ if (Array.isArray(value)) {
129
+ value.forEach((item: SearchResultItem) => {
130
+ appendDatalistOption(datalistElement, item, valueSchema, displaySchema, `${key}: `);
131
+ });
132
+ }
133
+ });
134
+ }
135
+
136
+ filterDatalist(datalistElement, searchTerm);
137
+ } catch (error) {
138
+ console.log(error);
139
+ }
140
+ };
141
+
142
+ export const filterDatalist = (datalistElement: HTMLDataListElement, searchTerm: string): void => {
143
+ for (const optionElement of datalistElement.options) {
144
+ const optionText = optionElement.textContent?.trim() || optionElement.value;
145
+ if (optionText.toLowerCase().includes(searchTerm.toLowerCase())) {
146
+ optionElement.classList.remove('js-hide');
147
+ } else {
148
+ optionElement.classList.add('js-hide');
149
+ }
150
+ }
151
+ };
152
+
153
+ export const datalistSelectOption = (
154
+ component: SearchComponent,
155
+ inputElement: HTMLInputElement,
156
+ optionElement: HTMLOptionElement
157
+ ): void => {
158
+ const datalistElement = optionElement.closest<HTMLDataListElement>('datalist');
159
+ const optionText = optionElement.textContent?.trim() || optionElement.value;
160
+ const inputName = inputElement.getAttribute('name') || '';
161
+ const alternateInputName = `${inputName}Alt`;
162
+
163
+ inputElement.value = optionText;
164
+ inputElement.setAttribute('data-value', optionText);
165
+ //inputElement.setAttribute('data-placeholder', optionText);
166
+ inputElement.setAttribute('placeholder', optionText);
167
+
168
+ // Make sure the value of the option is passed when in a form
169
+ if (optionElement.value && optionElement.value !== optionText) {
170
+
171
+
172
+ const alternateInput = component.querySelector<HTMLInputElement>(`input[name="${alternateInputName}"]`);
173
+
174
+ if (!alternateInput)
175
+ component.insertAdjacentHTML(
176
+ 'beforeend',
177
+ `<input type="hidden" name="${alternateInputName}" value="${optionElement.value}">`
178
+ );
179
+ else alternateInput.value = optionElement.value;
180
+ } else {
181
+
182
+ const alternateInput = component.querySelector<HTMLInputElement>(`input[name="${alternateInputName}"]`);
183
+
184
+ if (alternateInput) alternateInput.remove();
185
+ }
186
+
187
+ // Set the active value on the datalist option
188
+ if (!datalistElement) return;
189
+ for (const optionLoopElement of datalistElement.options) {
190
+ if (optionLoopElement === optionElement) optionLoopElement.classList.add('active');
191
+ else optionLoopElement.classList.remove('active');
192
+ }
193
+
194
+
195
+ const customEvent = new CustomEvent<OptionSelectedDetail>('option-selected', {
196
+ detail: {
197
+ title: optionText,
198
+ value: optionElement.value || '',
199
+ url: optionElement.hasAttribute('data-url') ? optionElement.getAttribute('data-url') || '' : '',
200
+ },
201
+ });
202
+
203
+ component.dispatchEvent(customEvent);
204
+ };
205
+
206
+ export default search;
@@ -1,27 +1,94 @@
1
1
  import { describe, expect, it } from './test.ts';
2
2
  import { createElement, installTestDom } from './test-dom.ts';
3
3
  import { append } from './test-utils.ts';
4
- import { openFirstTab, toggleTab } from './tabs.ts';
4
+ import { createTabsLinks, openFirstTab, setTabsEventHandlers, toggleTab } from './tabs.ts';
5
5
 
6
- installTestDom();
6
+ const { window } = installTestDom();
7
+
8
+ const createDetail = (id, title, isOpen = false) => {
9
+ const detail = createElement('details', { id });
10
+ const summary = createElement('summary', {}, title);
11
+
12
+ if (isOpen) detail.setAttribute('open', 'true');
13
+
14
+ append(detail, summary, createElement('p', {}, `${title} content`));
15
+
16
+ return detail;
17
+ };
7
18
 
8
19
  describe('Tabs module', () => {
9
- it('toggles tabs and opens the first tab by default', () => {
20
+ it('creates tab buttons and dropdown options from direct details', () => {
21
+ const tabs = createElement('iam-tabs', { class: 'tabs--toggle-tags' });
22
+ const first = createDetail('first', 'First tab', true);
23
+ const second = createDetail('second', 'Second tab');
24
+ append(tabs, first, second);
25
+
26
+ createTabsLinks(tabs);
27
+
28
+ const buttons = tabs.querySelectorAll('.tabs__links > button');
29
+ const options = tabs.querySelectorAll('.tabs__dropdown > option');
30
+
31
+ expect(buttons.length === 2);
32
+ expect(buttons[0].textContent === 'First tab');
33
+ expect(buttons[0].getAttribute('aria-pressed') === 'true');
34
+ expect(buttons[0].classList.contains('tag'));
35
+ expect(buttons[0].getAttribute('data-id') === 'first');
36
+ expect(first.getAttribute('tabindex') === '-1');
37
+ expect(first.querySelector('summary').classList.contains('visually-hidden'));
38
+ expect(options[0].value === 'first-tab');
39
+ });
40
+
41
+ it('toggles matching detail panels and records tab open events', () => {
42
+ window.dataLayer = [];
43
+ const tabs = createElement('iam-tabs');
44
+ const first = createDetail('first', 'First tab', true);
45
+ const second = createDetail('second', 'Second tab');
46
+ const linksWrapper = createElement('div');
47
+ const links = createElement('div', { class: 'tabs__links' });
48
+ const dropdownWrapper = createElement('div');
49
+ const dropdown = createElement('select', { class: 'tabs__dropdown' });
50
+ append(links, createElement('button', { ariaPressed: 'true', dataIndex: '0' }, 'First tab'));
51
+ append(links, createElement('button', { dataIndex: '1' }, 'Second tab'));
52
+ append(linksWrapper, links);
53
+ append(dropdownWrapper, dropdown);
54
+ append(tabs, linksWrapper, dropdownWrapper, first, second);
55
+
56
+ setTabsEventHandlers(tabs);
57
+
58
+ const buttons = tabs.querySelectorAll('.tabs__links > button');
59
+ buttons[1].click();
60
+
61
+ expect(!first.hasAttribute('open'));
62
+ expect(second.getAttribute('open') === 'true');
63
+ expect(buttons[1].getAttribute('aria-pressed') === 'true');
64
+ expect(window.dataLayer[0].event === 'openTab');
65
+ expect(window.dataLayer[0].tabTitle === 'Second tab');
66
+ });
67
+
68
+ it('opens the tab matching the current location hash', () => {
10
69
  const tabs = createElement('iam-tabs');
11
- const first = createElement('details');
12
- const second = createElement('details');
13
- const buttonOne = createElement('button', { dataIndex: '0' });
14
- const buttonTwo = createElement('button', { dataIndex: '1' });
15
70
  tabs.shadowRoot = createElement('shadow-root');
16
71
  const links = createElement('div', { class: 'tabs__links' });
17
- append(links, buttonOne, buttonTwo);
72
+ append(links, createElement('button', { dataId: 'first' }), createElement('button', { dataId: 'second' }));
18
73
  append(tabs.shadowRoot, links);
19
- append(tabs, first, second);
74
+ append(tabs, createDetail('first', 'First tab'), createDetail('second', 'Second tab'));
75
+ window.location.hash = '#second';
20
76
 
21
- toggleTab([first, second], buttonTwo);
22
77
  openFirstTab(tabs);
23
78
 
24
- expect(!first.hasAttribute('open'));
25
- expect(second.hasAttribute('open'));
79
+ expect(tabs.querySelector('details[id="second"]').getAttribute('open') === 'true');
80
+ expect(tabs.shadowRoot.querySelector('[data-id="second"]').getAttribute('aria-pressed') === 'true');
81
+
82
+ window.location.hash = '';
83
+ });
84
+
85
+ it('toggles detail panels directly from a selected tab button', () => {
86
+ const details = [createDetail('first', 'First tab'), createDetail('second', 'Second tab')];
87
+ const button = createElement('button', { dataIndex: '1' });
88
+
89
+ toggleTab(details, button);
90
+
91
+ expect(!details[0].hasAttribute('open'));
92
+ expect(details[1].getAttribute('open') === 'true');
26
93
  });
27
94
  });