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

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 (206) 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/applied-filters.css +1 -1
  8. package/assets/css/components/applied-filters.css.map +1 -1
  9. package/assets/css/components/banner.preload.css +1 -0
  10. package/assets/css/components/banner.preload.css.map +1 -0
  11. package/assets/css/components/calendar.component.css +1 -1
  12. package/assets/css/components/calendar.component.css.map +1 -1
  13. package/assets/css/components/card.component.css +1 -1
  14. package/assets/css/components/card.component.css.map +1 -1
  15. package/assets/css/components/card.module.css +1 -1
  16. package/assets/css/components/card.module.css.map +1 -1
  17. package/assets/css/components/card.preload.css +1 -0
  18. package/assets/css/components/card.preload.css.map +1 -0
  19. package/assets/css/components/carousel.component.css +1 -1
  20. package/assets/css/components/carousel.component.css.map +1 -1
  21. package/assets/css/components/carousel.config.css +1 -1
  22. package/assets/css/components/carousel.config.css.map +1 -1
  23. package/assets/css/components/config.component.css +1 -1
  24. package/assets/css/components/config.component.css.map +1 -1
  25. package/assets/css/components/content.component.css +1 -1
  26. package/assets/css/components/content.component.css.map +1 -1
  27. package/assets/css/components/fileupload.css +1 -1
  28. package/assets/css/components/fileupload.css.map +1 -1
  29. package/assets/css/components/filter-card.component.css +1 -1
  30. package/assets/css/components/filter-card.component.css.map +1 -1
  31. package/assets/css/components/multiselect.css +1 -1
  32. package/assets/css/components/multiselect.css.map +1 -1
  33. package/assets/css/components/nav.component.css +1 -1
  34. package/assets/css/components/nav.component.css.map +1 -1
  35. package/assets/css/components/pagination.css +1 -1
  36. package/assets/css/components/pagination.css.map +1 -1
  37. package/assets/css/components/record-card.component.css +1 -1
  38. package/assets/css/components/record-card.component.css.map +1 -1
  39. package/assets/css/components/search.component.css +1 -1
  40. package/assets/css/components/search.component.css.map +1 -1
  41. package/assets/css/components/skeleton.global.css +1 -1
  42. package/assets/css/components/skeleton.global.css.map +1 -1
  43. package/assets/css/components/slider.css +1 -1
  44. package/assets/css/components/slider.css.map +1 -1
  45. package/assets/css/components/split-button.component.css +1 -1
  46. package/assets/css/components/split-button.component.css.map +1 -1
  47. package/assets/css/components/std-nav-standalone.component.css +1 -1
  48. package/assets/css/components/std-nav-standalone.component.css.map +1 -1
  49. package/assets/css/components/tabs.component.css +1 -1
  50. package/assets/css/components/tabs.component.css.map +1 -1
  51. package/assets/css/components/tag.component.css +1 -1
  52. package/assets/css/components/tag.component.css.map +1 -1
  53. package/assets/css/components/video-card.component.css +1 -1
  54. package/assets/css/components/video-card.component.css.map +1 -1
  55. package/assets/css/core.min.css +1 -1
  56. package/assets/css/core.min.css.map +1 -1
  57. package/assets/css/elements/badge-tag.css +1 -1
  58. package/assets/css/elements/badge-tag.css.map +1 -1
  59. package/assets/css/elements/dropdown.css +1 -1
  60. package/assets/css/elements/dropdown.css.map +1 -1
  61. package/assets/css/elements/forms.css +1 -1
  62. package/assets/css/elements/forms.css.map +1 -1
  63. package/assets/css/elements/links--global.css +1 -1
  64. package/assets/css/elements/links--global.css.map +1 -1
  65. package/assets/css/elements/links.css +1 -1
  66. package/assets/css/elements/links.css.map +1 -1
  67. package/assets/css/style.min.css +1 -1
  68. package/assets/css/style.min.css.map +1 -1
  69. package/assets/js/components/accordion/accordion.component.min.js +1 -1
  70. package/assets/js/components/actionbar/actionbar.component.min.js +2 -2
  71. package/assets/js/components/address-lookup/address-lookup.component.min.js +4 -4
  72. package/assets/js/components/address-lookup/address-lookup.component.min.js.map +1 -1
  73. package/assets/js/components/advanced-select/advanced-select.component.min.js +2 -2
  74. package/assets/js/components/applied-filters/applied-filters.component.min.js +2 -2
  75. package/assets/js/components/banner/banner.component.min.js +1 -1
  76. package/assets/js/components/barchart/barchart.component.min.js +1 -1
  77. package/assets/js/components/bento-grid/bento-grid.component.min.js +1 -1
  78. package/assets/js/components/bone/bone.component.min.js +1 -1
  79. package/assets/js/components/button/button.component.min.js +1 -1
  80. package/assets/js/components/calendar/calendar.component.min.js +2 -2
  81. package/assets/js/components/card/card.component.js +114 -125
  82. package/assets/js/components/card/card.component.min.js +7 -7
  83. package/assets/js/components/card/card.component.min.js.map +1 -1
  84. package/assets/js/components/carousel/carousel.component.js +83 -29
  85. package/assets/js/components/carousel/carousel.component.min.js +16 -11
  86. package/assets/js/components/carousel/carousel.component.min.js.map +1 -1
  87. package/assets/js/components/collapsible-side/collapsible-side.component.min.js +1 -1
  88. package/assets/js/components/config/config.component.min.js +7 -7
  89. package/assets/js/components/config/config.component.min.js.map +1 -1
  90. package/assets/js/components/content/content.component.js +28 -69
  91. package/assets/js/components/content/content.component.min.js +4 -4
  92. package/assets/js/components/content/content.component.min.js.map +1 -1
  93. package/assets/js/components/darkmode/darkmode.component.min.js +1 -1
  94. package/assets/js/components/doughnutchart/doughnutchart.component.min.js +1 -1
  95. package/assets/js/components/fileupload/fileupload.component.min.js +2 -2
  96. package/assets/js/components/filter-card/filter-card.component.min.js +5 -5
  97. package/assets/js/components/filter-card/filter-card.component.min.js.map +1 -1
  98. package/assets/js/components/filterlist/filterlist.component.min.js +1 -1
  99. package/assets/js/components/form/form.component.min.js +1 -1
  100. package/assets/js/components/header/header.component.min.js +1 -1
  101. package/assets/js/components/inline-edit/inline-edit.component.min.js +1 -1
  102. package/assets/js/components/input/input.component.min.js +1 -1
  103. package/assets/js/components/input-range/input-range.component.min.js +1 -1
  104. package/assets/js/components/marketing/marketing.component.min.js +1 -1
  105. package/assets/js/components/menu/menu.component.min.js +1 -1
  106. package/assets/js/components/milestone/milestone.component.min.js +1 -1
  107. package/assets/js/components/milestone-group/milestone-group.component.min.js +1 -1
  108. package/assets/js/components/modal/modal.component.min.js +1 -1
  109. package/assets/js/components/multi-step/multi-step.component.min.js +1 -1
  110. package/assets/js/components/multi-step-modal/multi-step-modal.component.min.js +1 -1
  111. package/assets/js/components/multiselect/multiselect.component.min.js +4 -4
  112. package/assets/js/components/multiselect/multiselect.component.min.js.map +1 -1
  113. package/assets/js/components/nav/nav.component.min.js +2 -2
  114. package/assets/js/components/notification/notification.component.min.js +1 -1
  115. package/assets/js/components/pagination/pagination.component.min.js +5 -5
  116. package/assets/js/components/password/password.component.min.js +1 -1
  117. package/assets/js/components/popover/popover.component.min.js +1 -1
  118. package/assets/js/components/rank/rank.component.min.js +1 -1
  119. package/assets/js/components/rankings/rankings.component.min.js +1 -1
  120. package/assets/js/components/rating/rating.component.min.js +1 -1
  121. package/assets/js/components/record-card/record-card.component.min.js +6 -6
  122. package/assets/js/components/record-card/record-card.component.min.js.map +1 -1
  123. package/assets/js/components/search/search.component.js +234 -186
  124. package/assets/js/components/search/search.component.min.js +12 -7
  125. package/assets/js/components/search/search.component.min.js.map +1 -1
  126. package/assets/js/components/skeleton/skeleton.component.min.js +1 -1
  127. package/assets/js/components/slider/slider.component.min.js +2 -2
  128. package/assets/js/components/split-button/split-button.component.min.js +2 -2
  129. package/assets/js/components/std-address-lookup/std-address-lookup.component.min.js +5 -5
  130. package/assets/js/components/std-address-lookup/std-address-lookup.component.min.js.map +1 -1
  131. package/assets/js/components/std-nav/std-nav.component.js +10 -9
  132. package/assets/js/components/std-nav/std-nav.component.min.js +9 -12
  133. package/assets/js/components/std-nav/std-nav.component.min.js.map +1 -1
  134. package/assets/js/components/std-nav-standalone/std-nav-standalone.component.min.js +5 -5
  135. package/assets/js/components/std-nav-standalone/std-nav-standalone.component.min.js.map +1 -1
  136. package/assets/js/components/table/table.component.min.js +1 -1
  137. package/assets/js/components/table-ajax/table-ajax.component.min.js +1 -1
  138. package/assets/js/components/table-basic/table-basic.component.min.js +1 -1
  139. package/assets/js/components/table-no-submit/table-no-submit.component.min.js +1 -1
  140. package/assets/js/components/table-submit/table-submit.component.min.js +1 -1
  141. package/assets/js/components/tabs/tabs.component.min.js +4 -4
  142. package/assets/js/components/tag/tag.component.min.js +3 -3
  143. package/assets/js/components/tag/tag.component.min.js.map +1 -1
  144. package/assets/js/components/tooltip/tooltip.component.min.js +1 -1
  145. package/assets/js/components/video/video.component.min.js +1 -1
  146. package/assets/js/components/video-card/video-card.component.min.js +9 -9
  147. package/assets/js/components/video-card/video-card.component.min.js.map +1 -1
  148. package/assets/js/components/video-modal/video-modal.component.min.js +1 -1
  149. package/assets/js/components/word-count/word-count.component.min.js +1 -1
  150. package/assets/js/modules/card.module.js +12 -11
  151. package/assets/js/modules/content.js +40 -8
  152. package/assets/js/modules/content.test.js +62 -12
  153. package/assets/js/modules/data-layer.js +7 -6
  154. package/assets/js/modules/dropdown.js +0 -1
  155. package/assets/js/modules/nav.js +10 -3
  156. package/assets/js/modules/search.js +153 -0
  157. package/assets/js/modules/search.test.js +125 -0
  158. package/assets/js/modules/tabs.test.js +64 -12
  159. package/assets/js/modules/testimonial.test.js +44 -6
  160. package/assets/js/modules/videos.test.js +61 -13
  161. package/assets/js/scripts.bundle.js +3 -3
  162. package/assets/js/scripts.bundle.js.map +1 -1
  163. package/assets/js/scripts.bundle.min.js +2 -2
  164. package/assets/js/scripts.bundle.min.js.map +1 -1
  165. package/assets/sass/_components.scss +2 -63
  166. package/assets/sass/components/banner.preload.scss +26 -0
  167. package/assets/sass/components/card.component.scss +1 -7
  168. package/assets/sass/components/card.module.scss +6 -6
  169. package/assets/sass/components/card.preload.scss +80 -0
  170. package/assets/sass/components/carousel.component.scss +165 -0
  171. package/assets/sass/components/carousel.config.scss +85 -249
  172. package/assets/sass/components/content.component.scss +0 -7
  173. package/assets/sass/components/nav.component.scss +2 -1
  174. package/assets/sass/components/search.component.scss +89 -7
  175. package/assets/sass/components/skeleton.global.scss +4 -0
  176. package/assets/sass/elements/badge-tag.css +1 -1
  177. package/assets/sass/elements/dropdown.css +2 -0
  178. package/assets/sass/elements/forms.scss +0 -27
  179. package/assets/sass/elements/links--global.scss +40 -2
  180. package/assets/sass/foundations/root.scss +0 -1
  181. package/assets/sass/utilities/border.css +1 -1
  182. package/assets/sass/utilities/js-display.css +2 -3
  183. package/assets/ts/components/card/card.component.ts +72 -62
  184. package/assets/ts/components/carousel/carousel.component.ts +84 -19
  185. package/assets/ts/components/content/content.component.ts +36 -100
  186. package/assets/ts/components/search/search.component.ts +257 -185
  187. package/assets/ts/components/std-nav/std-nav.component.ts +17 -16
  188. package/assets/ts/html.d.ts +6 -0
  189. package/assets/ts/modules/card.module.ts +20 -12
  190. package/assets/ts/modules/content.test.ts +84 -12
  191. package/assets/ts/modules/content.ts +56 -9
  192. package/assets/ts/modules/data-layer.ts +7 -11
  193. package/assets/ts/modules/dropdown.ts +0 -2
  194. package/assets/ts/modules/nav.ts +12 -3
  195. package/assets/ts/modules/search.test.ts +142 -0
  196. package/assets/ts/modules/search.ts +206 -0
  197. package/assets/ts/modules/tabs.test.ts +79 -12
  198. package/assets/ts/modules/testimonial.test.ts +45 -6
  199. package/assets/ts/modules/videos.test.ts +74 -14
  200. package/dist/components.es.js +25 -25
  201. package/dist/components.umd.js +164 -157
  202. package/package.json +1 -1
  203. package/assets/js/modules/carousel.js +0 -214
  204. package/assets/js/modules/carousel.test.js +0 -18
  205. package/assets/ts/modules/carousel.test.ts +0 -27
  206. package/assets/ts/modules/carousel.ts +0 -301
@@ -16,34 +16,42 @@ export const cardHTML = `<div class="wrapper">
16
16
  </div>
17
17
  </div>`;
18
18
 
19
- export const setupCard = (cardComponent: any): void => {
19
+ const getCardPart = <T extends Element>(cardComponent: HTMLElement, selector: string): T | null =>
20
+ cardComponent.shadowRoot?.querySelector<T>(selector) || null;
21
+
22
+ export const setupCard = (cardComponent: HTMLElement): void => {
20
23
  cardComponent.classList.add('card');
21
- const cardHead = cardComponent.shadowRoot.querySelector('.card__head');
22
- const cardBody = cardComponent.shadowRoot.querySelector('.card__body');
24
+ const cardHead = getCardPart<HTMLDivElement>(cardComponent, '.card__head');
25
+ const cardBody = getCardPart<HTMLDivElement>(cardComponent, '.card__body');
26
+ const cardBadges = getCardPart<HTMLDivElement>(cardComponent, '.card__badges');
23
27
 
24
28
  if (cardComponent.hasAttribute('data-image')) {
25
- cardHead.innerHTML += `<img src="${cardComponent.getAttribute('data-image')}" alt="" loading="lazy" part="image" />`;
29
+ cardHead?.insertAdjacentHTML(
30
+ 'beforeend',
31
+ `<img src="${cardComponent.getAttribute('data-image') || ''}" alt="" loading="lazy" part="image" />`
32
+ );
26
33
  }
27
34
 
28
35
  // Inset the HTML for the data total or icon fallback
29
36
  if (cardComponent.hasAttribute('data-total')) {
30
37
 
31
- if(!cardBody?.querySelector('.card__total'))
32
- cardBody.insertAdjacentHTML(
38
+ const cardTotal = cardBody?.querySelector<HTMLDivElement>('.card__total');
39
+
40
+ if(!cardTotal)
41
+ cardBody?.insertAdjacentHTML(
33
42
  'beforeend',
34
- `<div class="card__total">${cardComponent.getAttribute('data-total')}</div>`
43
+ `<div class="card__total">${cardComponent.getAttribute('data-total') || ''}</div>`
35
44
  );
36
45
  else {
37
- const cardTotal = cardBody?.querySelector('.card__total');
38
- if (cardTotal) cardTotal.innerHTML = cardComponent.getAttribute('data-total');
46
+ cardTotal.innerHTML = cardComponent.getAttribute('data-total') || '';
39
47
  }
40
48
  } else if (cardComponent.querySelector('[slot="total-icon"]')) {
41
- cardBody.insertAdjacentHTML('beforeend', `<div class="card__total"><slot name="total-icon"></slot></div>`);
49
+ cardBody?.insertAdjacentHTML('beforeend', `<div class="card__total"><slot name="total-icon"></slot></div>`);
42
50
  }
43
51
 
44
52
  if (!cardComponent.querySelector('[slot="badges"]')) {
45
- cardComponent.shadowRoot.querySelector('.card__badges').classList.add('empty');
53
+ cardBadges?.classList.add('empty');
46
54
  } else {
47
- cardComponent.shadowRoot.querySelector('.card__badges').classList.remove('empty');
55
+ cardBadges?.classList.remove('empty');
48
56
  }
49
57
  };
@@ -1,24 +1,96 @@
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 { transformButtons } from './content.ts';
4
+ import { createTitle, loadComponents, replaceShortcode, transformButtons, transformElement } from './content.ts';
5
5
 
6
- const { document } = installTestDom();
6
+ installTestDom();
7
+
8
+ type TestAttribute = {
9
+ name: string;
10
+ value: string;
11
+ };
12
+
13
+ type AttributeComponent = {
14
+ attributes: TestAttribute[];
15
+ getAttribute: (name: string) => string | null;
16
+ hasAttribute: (name: string) => boolean;
17
+ };
18
+
19
+ const createAttributeComponent = (attributes: TestAttribute[]): AttributeComponent => ({
20
+ attributes,
21
+ getAttribute(name: string): string | null {
22
+ const attribute = attributes.find((attr) => attr.name === name);
23
+ return attribute ? attribute.value : null;
24
+ },
25
+ hasAttribute(name: string): boolean {
26
+ return attributes.some((attr) => attr.name === name);
27
+ },
28
+ });
7
29
 
8
30
  describe('Content module', () => {
31
+ it('replaces WordPress shortcode paragraphs with loading placeholders', () => {
32
+ const content = '<p>Intro</p><p>[search-learning-articles]</p><p>[search-contacts]</p>';
33
+ const transformed = replaceShortcode(content);
34
+
35
+ expect(transformed.includes('<p>Intro</p>'));
36
+ expect(transformed.includes('data-shortcode="search-learning-articles"'));
37
+ expect(transformed.includes('data-shortcode="search-contacts"'));
38
+ expect(!transformed.includes('<p>[search-learning-articles]</p>'));
39
+ expect(!transformed.includes('<p>[search-contacts]</p>'));
40
+ });
41
+
42
+ it('creates configured titles and skips missing titles', () => {
43
+ const component = createElement('iam-content', {
44
+ dataTitleTag: 'h2',
45
+ dataTitleClass: 'bg-light',
46
+ });
47
+
48
+ expect(createTitle(component, 'Latest news') === '<h2 class="bg-light iam-content--title">Latest news</h2>');
49
+ expect(createTitle(component, '') === '');
50
+ expect(createTitle(createElement('iam-content'), 'Latest news') === '');
51
+ });
52
+
53
+ it('wraps rendered content in the requested transform element', () => {
54
+ const component = createAttributeComponent([
55
+ { name: 'data-transform', value: 'article' },
56
+ { name: 'data-url', value: '/wp-json/wp/v2/pages/1' },
57
+ { name: 'class', value: 'content-panel' },
58
+ ]);
59
+
60
+ const transformed = transformElement(component, '<h2>Heading</h2>', '<p>Body</p>');
61
+
62
+ expect(transformed.startsWith('<article '));
63
+ expect(transformed.includes('data-transform="article"'));
64
+ expect(transformed.includes('data-url="/wp-json/wp/v2/pages/1"'));
65
+ expect(transformed.includes('class="content-panel"'));
66
+ expect(transformed.includes('<h2>Heading</h2><p>Body</p>'));
67
+ expect(transformed.endsWith('</article>'));
68
+ });
69
+
9
70
  it('transforms WordPress button wrappers into direct links', () => {
10
- const parent = createElement('div');
11
71
  const buttons = createElement('div', { class: 'wp-block-buttons' });
12
- const button = createElement('div', { class: 'btn btn-primary wp-block-button' });
13
- const link = createElement('a', { href: '/test' }, 'Open');
14
- append(button, link);
15
- append(buttons, button);
16
- append(parent, buttons);
17
- append(document.body, parent);
72
+ const primaryButton = createElement('div', { class: 'btn btn-primary wp-block-button' });
73
+ const secondaryButton = createElement('div', { class: 'btn btn-secondary wp-block-button' });
74
+ const primaryLink = createElement('a', { href: '/primary' }, 'Primary');
75
+ const secondaryLink = createElement('a', { href: '/secondary' }, 'Secondary');
76
+ append(primaryButton, primaryLink);
77
+ append(secondaryButton, secondaryLink);
78
+ append(buttons, primaryButton, secondaryButton);
79
+
80
+ const fragment = transformButtons(buttons);
81
+
82
+ expect(fragment.children.length === 2);
83
+ expect(fragment.children[0] === primaryLink);
84
+ expect(fragment.children[1] === secondaryLink);
85
+ expect(primaryLink.getAttribute('class') === 'btn btn-primary wp-block-button');
86
+ expect(secondaryLink.getAttribute('class') === 'btn btn-secondary wp-block-button');
87
+ });
88
+
89
+ it('does not load component bundles when content contains no supported custom elements', () => {
90
+ const component = createElement('iam-content');
18
91
 
19
- transformButtons();
92
+ loadComponents(component);
20
93
 
21
- expect(link.getAttribute('class') === 'btn btn-primary wp-block-button');
22
- expect(parent.children[0].localName === 'fragment');
94
+ expect(component.getElementsByTagName('iam-card').length === 0);
23
95
  });
24
96
  });
@@ -1,17 +1,64 @@
1
- export const transformButtons = (component):void => {
2
1
 
3
- Array.from(document.querySelectorAll('.wp-block-buttons')).forEach((buttons) => {
4
2
 
5
- const fragment = document.createDocumentFragment();
3
+ export const loadComponents = (component):void => {
6
4
 
7
- Array.from(buttons.querySelectorAll('.wp-block-button')).forEach((element) => {
5
+ const components = ['skeleton','bone','carousel', 'card', 'banner', 'notification'];
6
+ const assetLocation = document.body.hasAttribute('data-assets-location')
7
+ ? document.body.getAttribute('data-assets-location')
8
+ : '/assets';
8
9
 
9
- const link = element.querySelector('a');
10
- link.setAttribute('class',element.getAttribute('class'));
10
+ components.forEach((loadComponent) => {
11
11
 
12
- fragment.appendChild(link);
13
- });
12
+ if (component.getElementsByTagName(`iam-${loadComponent}`).length === 0) return;
14
13
 
15
- buttons.parentNode.replaceChild(fragment, buttons);
14
+ import(/*! @vite-ignore */ `${assetLocation}/js/components/${loadComponent}/${loadComponent}.component.min.js`)
15
+ .then((module) => {
16
+ if (!window.customElements.get(`iam-${loadComponent}`))
17
+ window.customElements.define(`iam-${loadComponent}`, module.default);
18
+ })
19
+ .catch((err) => {
20
+ console.log(err.message);
21
+ });
16
22
  });
17
23
  }
24
+
25
+ export const replaceShortcode = (content):string => {
26
+
27
+ return content.replaceAll(/<p>\[([^\]]+)\]<\/p>/g, "<span data-shortcode=\"$1\"><iam-skeleton><iam-bone class=\"search\"></iam-bone></iam-skeleton></span>");
28
+ }
29
+
30
+ export const createTitle = (component, title):string => {
31
+
32
+ if (component.hasAttribute('data-title-tag') && title)
33
+ return `<${component.getAttribute('data-title-tag')} class="${component.getAttribute('data-title-class')} iam-content--title">${title}</${component.getAttribute('data-title-tag')}>`;
34
+
35
+ return '';
36
+ }
37
+
38
+ export const transformElement = (component,renderedTitle,renderedContent): string => {
39
+
40
+ const transform = component.getAttribute('data-transform');
41
+
42
+ let elementAttributes = '';
43
+
44
+ for (const attr of component.attributes) {
45
+ elementAttributes += `${attr.name}="${attr.value}" `;
46
+ }
47
+
48
+ return `<${transform} ${elementAttributes}>${renderedTitle+renderedContent}</${transform}>`;
49
+ }
50
+
51
+ export const transformButtons = (buttons): DocumentFragment => {
52
+
53
+ const fragment = document.createDocumentFragment();
54
+
55
+ Array.from(buttons.querySelectorAll('.wp-block-button')).forEach((element) => {
56
+
57
+ const link = element.querySelector('a');
58
+ link.setAttribute('class',element.getAttribute('class'));
59
+
60
+ fragment.appendChild(link);
61
+ });
62
+
63
+ return fragment;
64
+ }
@@ -1,12 +1,8 @@
1
- type WindowWithDataLayer = Window & {
2
- dataLayer: Record<string, any>[];
3
- };
4
-
5
- declare const window: WindowWithDataLayer;
1
+ const dataLayerWindow = window as WindowWithDataLayer;
6
2
 
7
3
  function createDataLayer(): void {
8
- window.dataLayer = window.dataLayer || [];
9
- window.dataLayer.push({
4
+ dataLayerWindow.dataLayer = dataLayerWindow.dataLayer || [];
5
+ dataLayerWindow.dataLayer.push({
10
6
  event: 'Pageview',
11
7
  pageTitle: document.title,
12
8
  });
@@ -15,7 +11,7 @@ function createDataLayer(): void {
15
11
  const target = (event.target as HTMLElement).closest<HTMLElement>('[open] summary');
16
12
 
17
13
  if (target) {
18
- window.dataLayer.push({
14
+ dataLayerWindow.dataLayer.push({
19
15
  event: 'closeDetails',
20
16
  detailsTitle: target.textContent || '',
21
17
  });
@@ -25,14 +21,14 @@ function createDataLayer(): void {
25
21
  const button = (event.target as HTMLElement).closest<HTMLButtonElement>('button');
26
22
 
27
23
  if (summary) {
28
- window.dataLayer.push({
24
+ dataLayerWindow.dataLayer.push({
29
25
  event: 'openDetails',
30
26
  detailsTitle: summary.textContent || '',
31
27
  });
32
28
  }
33
29
 
34
30
  if (link) {
35
- window.dataLayer.push({
31
+ dataLayerWindow.dataLayer.push({
36
32
  event: 'linkClicked',
37
33
  linkText: link.hasAttribute('title') ? link.getAttribute('title') || '' : link.textContent || '',
38
34
  class: link.hasAttribute('class') ? link.getAttribute('class') || '' : '',
@@ -41,7 +37,7 @@ function createDataLayer(): void {
41
37
  }
42
38
 
43
39
  if (button) {
44
- window.dataLayer.push({
40
+ dataLayerWindow.dataLayer.push({
45
41
  event: 'buttonClicked',
46
42
  buttonText: button.textContent || '',
47
43
  class: button.hasAttribute('class') ? button.getAttribute('class') || '' : '',
@@ -25,8 +25,6 @@ export const searchAjax = async (component, search, callback): any => {
25
25
  const ajaxURL = component.getAttribute('data-url');
26
26
  const firstInput = component.querySelector('input');
27
27
 
28
- console.log(firstInput);
29
-
30
28
  const inputType = firstInput && firstInput.hasAttribute('type') ? firstInput.getAttribute('type') : 'checkbox';
31
29
  let inputName = firstInput && firstInput.hasAttribute('name') ? firstInput.getAttribute('name') : 'tags';
32
30
 
@@ -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;