@bridgeline-digital/hawksearch-handlebars-ui 1.0.0-beta.0

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 (118) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +153 -0
  3. package/.husky/pre-commit +5 -0
  4. package/.lintstagedrc.json +4 -0
  5. package/.prettierignore +7 -0
  6. package/.prettierrc.json +19 -0
  7. package/.vscode/launch.json +15 -0
  8. package/custom1.html +52 -0
  9. package/custom2.html +44 -0
  10. package/gulpfile.js +73 -0
  11. package/index.html +39 -0
  12. package/package.json +57 -0
  13. package/src/assets/icons/chevron-back.svg +4 -0
  14. package/src/assets/icons/chevron-down.svg +3 -0
  15. package/src/assets/icons/chevron-forward.svg +4 -0
  16. package/src/assets/icons/chevron-left.svg +3 -0
  17. package/src/assets/icons/chevron-right.svg +3 -0
  18. package/src/assets/icons/chevron-up.svg +3 -0
  19. package/src/assets/icons/cross.svg +3 -0
  20. package/src/assets/icons/help.svg +4 -0
  21. package/src/assets/icons/minus.svg +3 -0
  22. package/src/assets/icons/plus.svg +7 -0
  23. package/src/assets/images/icons.svg +1 -0
  24. package/src/assets/images/placeholder.png +0 -0
  25. package/src/components/autocomplete/autocomplete.component.hbs +60 -0
  26. package/src/components/autocomplete/autocomplete.component.ts +124 -0
  27. package/src/components/base.component.ts +120 -0
  28. package/src/components/facet-types/base-facet.component.ts +114 -0
  29. package/src/components/facet-types/checkbox-list-facet/checkbox-list-facet.component.hbs +6 -0
  30. package/src/components/facet-types/checkbox-list-facet/checkbox-list-facet.component.ts +73 -0
  31. package/src/components/facet-types/checkbox-list-facet/checkbox-list.partial.hbs +42 -0
  32. package/src/components/facet-types/color-facet/color-facet.component.hbs +29 -0
  33. package/src/components/facet-types/color-facet/color-facet.component.ts +26 -0
  34. package/src/components/facet-types/index.ts +6 -0
  35. package/src/components/facet-types/linked-list-facet/link-list-facet.component.hbs +17 -0
  36. package/src/components/facet-types/linked-list-facet/link-list-facet.component.ts +32 -0
  37. package/src/components/facet-types/range-slider-facet/range-slider-facet.component.hbs +11 -0
  38. package/src/components/facet-types/range-slider-facet/range-slider-facet.component.ts +107 -0
  39. package/src/components/facet-types/search-within-facet/search-within-facet.component.hbs +3 -0
  40. package/src/components/facet-types/search-within-facet/search-within-facet.component.ts +33 -0
  41. package/src/components/facet-types/size-facet/size-facet.component.hbs +29 -0
  42. package/src/components/facet-types/size-facet/size-facet.component.ts +30 -0
  43. package/src/components/facet-wrapper/facet-wrapper.component.hbs +36 -0
  44. package/src/components/facet-wrapper/facet-wrapper.component.ts +62 -0
  45. package/src/components/facets-list/facets-list.component.hbs +11 -0
  46. package/src/components/facets-list/facets-list.component.ts +92 -0
  47. package/src/components/icon/icon.component.hbs +3 -0
  48. package/src/components/icon/icon.component.ts +40 -0
  49. package/src/components/index.ts +18 -0
  50. package/src/components/modified-query/modified-query.component.hbs +7 -0
  51. package/src/components/modified-query/modified-query.component.ts +38 -0
  52. package/src/components/page-size/page-size.component.hbs +7 -0
  53. package/src/components/page-size/page-size.component.ts +35 -0
  54. package/src/components/pagination/pagination.component.hbs +34 -0
  55. package/src/components/pagination/pagination.component.ts +85 -0
  56. package/src/components/query-suggestions/query-suggestions.component.hbs +3 -0
  57. package/src/components/query-suggestions/query-suggestions.component.ts +66 -0
  58. package/src/components/range-slider/range-slider.component.hbs +9 -0
  59. package/src/components/range-slider/range-slider.component.ts +190 -0
  60. package/src/components/search/search.component.hbs +4 -0
  61. package/src/components/search/search.component.ts +93 -0
  62. package/src/components/search-results/search-results.component.hbs +29 -0
  63. package/src/components/search-results/search-results.component.ts +17 -0
  64. package/src/components/search-results-item/search-results-item.component.hbs +23 -0
  65. package/src/components/search-results-item/search-results-item.component.ts +99 -0
  66. package/src/components/search-results-list/search-results-list.component.hbs +15 -0
  67. package/src/components/search-results-list/search-results-list.component.ts +24 -0
  68. package/src/components/selected-facets/selected-facets.component.hbs +11 -0
  69. package/src/components/selected-facets/selected-facets.component.ts +81 -0
  70. package/src/components/sorting/sorting.component.hbs +7 -0
  71. package/src/components/sorting/sorting.component.ts +33 -0
  72. package/src/components/tabs/tabs.component.hbs +7 -0
  73. package/src/components/tabs/tabs.component.ts +35 -0
  74. package/src/components/tooltip/tooltip.component.hbs +6 -0
  75. package/src/components/tooltip/tooltip.component.ts +48 -0
  76. package/src/helpers/formatters.ts +15 -0
  77. package/src/helpers/index.ts +1 -0
  78. package/src/index.ts +125 -0
  79. package/src/models/autocomplete.models.ts +71 -0
  80. package/src/models/component.models.ts +27 -0
  81. package/src/models/global.models.ts +78 -0
  82. package/src/models/index.ts +7 -0
  83. package/src/models/raw-autocomplete.models.ts +46 -0
  84. package/src/models/raw-search.models.ts +130 -0
  85. package/src/models/search.models.ts +144 -0
  86. package/src/models/tracking.models.ts +33 -0
  87. package/src/scss/components/_autocomplete.scss +55 -0
  88. package/src/scss/components/_facets.scss +287 -0
  89. package/src/scss/components/_icons.scss +17 -0
  90. package/src/scss/components/_modified-query.scss +11 -0
  91. package/src/scss/components/_page-size.scss +2 -0
  92. package/src/scss/components/_pagination.scss +23 -0
  93. package/src/scss/components/_query-suggestions.scss +7 -0
  94. package/src/scss/components/_range-slider.scss +50 -0
  95. package/src/scss/components/_search-results-item.scss +64 -0
  96. package/src/scss/components/_search.scss +4 -0
  97. package/src/scss/components/_selected-facets.scss +24 -0
  98. package/src/scss/components/_sorting.scss +2 -0
  99. package/src/scss/components/_tabs.scss +51 -0
  100. package/src/scss/components/_tooltips.scss +29 -0
  101. package/src/scss/general/_forms.scss +82 -0
  102. package/src/scss/general/_grid-layout.scss +44 -0
  103. package/src/scss/general/_images.scss +3 -0
  104. package/src/scss/general/_typography.scss +15 -0
  105. package/src/scss/hawksearch.scss +35 -0
  106. package/src/scss/includes/_breakpoints.scss +51 -0
  107. package/src/scss/includes/_components.scss +14 -0
  108. package/src/scss/includes/_general.scss +4 -0
  109. package/src/scss/includes/_reset.scss +203 -0
  110. package/src/scss/includes/_utility-classes.scss +85 -0
  111. package/src/services/autocomplete.service.ts +104 -0
  112. package/src/services/base.service.ts +93 -0
  113. package/src/services/index.ts +3 -0
  114. package/src/services/search.service.ts +754 -0
  115. package/src/services/tracking.service.ts +136 -0
  116. package/tsconfig.json +22 -0
  117. package/typings.d.ts +21 -0
  118. package/webpack.config.js +75 -0
@@ -0,0 +1,85 @@
1
+ import { Pagination } from '@models';
2
+ import { searchService } from '@services';
3
+ import { BaseComponent } from '../base.component';
4
+ import defaultHtml from './pagination.component.hbs';
5
+
6
+ export class PaginationComponent extends BaseComponent<Pagination> {
7
+ protected bindEvent = 'hawksearch:bind-pagination';
8
+ protected customTemplatePropertyName = 'pagination';
9
+ protected defaultHtml = defaultHtml;
10
+
11
+ private defaultMaxPageLinks = 9;
12
+
13
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
14
+ if (!this.data || this.data.totalResults == 0) {
15
+ return '';
16
+ }
17
+
18
+ const firstRecord = (this.data.page - 1) * this.data.pageSize + 1;
19
+ let lastRecord = firstRecord + this.data.pageSize - 1;
20
+
21
+ if (lastRecord > this.data.totalResults) {
22
+ lastRecord = this.data.totalResults;
23
+ }
24
+
25
+ const html = template({
26
+ page: this.data.page,
27
+ pages: this.getPages(),
28
+ firstPage: 1,
29
+ lastPage: this.data.totalPages,
30
+ firstRecord: firstRecord,
31
+ lastRecord: lastRecord,
32
+ totalRecords: this.data.totalResults ?? 0,
33
+ totalPages: this.data.totalPages ?? 1,
34
+ maxPageLinks: this.data.maxPageLinks ?? this.defaultMaxPageLinks,
35
+ displayFirstLink: this.data.displayFirstLink && this.data.page > 1,
36
+ displayPreviousLink: this.data.page > 1,
37
+ displayNextLink: this.data.page < this.data.totalPages,
38
+ displayLastLink: this.data.displayLastLink && this.data.page < this.data.totalPages,
39
+ summary: `Showing ${firstRecord}-${lastRecord} of ${this.data.totalResults}`
40
+ });
41
+
42
+ return html;
43
+ }
44
+
45
+ protected bindChildElements(): void {
46
+ this.shadowRoot?.querySelectorAll('[hawksearch-page]').forEach((e) => {
47
+ const value = e.getAttribute('hawksearch-page');
48
+
49
+ if (value) {
50
+ const page = parseInt(value);
51
+
52
+ e.addEventListener('click', () => this.setPage(page));
53
+ }
54
+ });
55
+ }
56
+
57
+ protected async setPage(page: number): Promise<void> {
58
+ await searchService.paginate(page);
59
+ }
60
+
61
+ private getPages(): Array<number> {
62
+ const pages: Array<number> = [];
63
+
64
+ if (this.data && this.data.totalPages > 1) {
65
+ const maxPages = this.data.maxPageLinks ?? this.defaultMaxPageLinks;
66
+ let firstPage = this.data.page - Math.floor(maxPages / 2);
67
+
68
+ if (firstPage < 1) {
69
+ firstPage = 1;
70
+ }
71
+
72
+ let lastPage = firstPage + maxPages - 1;
73
+
74
+ if (lastPage > this.data.totalPages) {
75
+ lastPage = this.data.totalPages;
76
+ }
77
+
78
+ for (let i = firstPage; i <= lastPage; i++) {
79
+ pages.push(i);
80
+ }
81
+ }
82
+
83
+ return pages;
84
+ }
85
+ }
@@ -0,0 +1,3 @@
1
+ <div class='query-suggestions'>
2
+ <p>Did you mean to search for {{query-suggestions querySuggestions}}?</p>
3
+ </div>
@@ -0,0 +1,66 @@
1
+ import { SafeString } from 'handlebars';
2
+ import { HawksearchGlobal, QuerySuggestionsData } from '@models';
3
+ import { searchService } from '@services';
4
+ import { BaseComponent } from '../base.component';
5
+ import defaultHtml from './query-suggestions.component.hbs';
6
+
7
+ declare let Hawksearch: HawksearchGlobal;
8
+
9
+ export class QuerySuggestionsComponent extends BaseComponent<QuerySuggestionsData> {
10
+ protected bindEvent = 'hawksearch:bind-query-suggestions';
11
+ protected customTemplatePropertyName = 'querySuggestions';
12
+ protected defaultHtml = defaultHtml;
13
+
14
+ protected registerHelpers(): void {
15
+ super.registerHelpers();
16
+
17
+ this.handlebars.registerHelper('query-suggestions', function (querySuggestions: Array<string>): SafeString {
18
+ function linkFunction(query: string): string {
19
+ return `<a hawksearch-query="${encodeURIComponent(query)}">${query}</a>`;
20
+ }
21
+
22
+ if (querySuggestions.length <= 2) {
23
+ return new Hawksearch.handlebars.SafeString(querySuggestions.map(linkFunction).join(' or '));
24
+ }
25
+
26
+ const generalDelimeter = ', ';
27
+ const lastDelimeter = ', or ';
28
+
29
+ return new Hawksearch.handlebars.SafeString(
30
+ querySuggestions
31
+ .slice(0, querySuggestions.length - 1)
32
+ .map(linkFunction)
33
+ .join(generalDelimeter) +
34
+ lastDelimeter +
35
+ linkFunction(querySuggestions[querySuggestions.length - 1])
36
+ );
37
+ });
38
+ }
39
+
40
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
41
+ if (!this.data || !this.data.display || !this.data.querySuggestions?.length) {
42
+ return '';
43
+ }
44
+
45
+ const html = template({
46
+ querySuggestions: this.data.querySuggestions
47
+ });
48
+
49
+ return html;
50
+ }
51
+
52
+ protected bindChildElements(): void {
53
+ this.shadowRoot?.querySelectorAll('[hawksearch-query]').forEach((e) => {
54
+ e.addEventListener('click', (event: Event) => {
55
+ event.preventDefault();
56
+
57
+ const element = event.currentTarget as HTMLElement;
58
+ const query = element.getAttribute('hawksearch-query');
59
+
60
+ if (query) {
61
+ searchService.query(query);
62
+ }
63
+ });
64
+ });
65
+ }
66
+ }
@@ -0,0 +1,9 @@
1
+ <div class='range-slider'>
2
+ <div class='range-slider__container'>
3
+ <div class='range-slider__bar'>
4
+ <div class='range-slider__bar__active' hawksearch-bar style='left: {{start}}%; right: {{subtract 100 end}}%;'></div>
5
+ </div>
6
+ <span class='range-slider__handle range-slider__handle--min' title='Minimum' hawksearch-handle='min' style='left: {{start}}%;'></span>
7
+ <span class='range-slider__handle range-slider__handle--max' title='Maximum' hawksearch-handle='max' style='left: {{end}}%;'></span>
8
+ </div>
9
+ </div>
@@ -0,0 +1,190 @@
1
+ import { RangeSliderEventData } from '@models';
2
+ import { BaseComponent } from '../base.component';
3
+ import defaultHtml from './range-slider.component.hbs';
4
+
5
+ export class RangeSliderComponent extends BaseComponent<never> {
6
+ static get observedAttributes(): Array<string> {
7
+ return ['min', 'max'];
8
+ }
9
+
10
+ protected bindEvent = undefined;
11
+ protected customTemplatePropertyName = 'rangeSlider';
12
+ protected defaultHtml = defaultHtml;
13
+
14
+ private dragging = false;
15
+ private activeHandle?: HTMLElement;
16
+ private startX?: number;
17
+ private startOffsetPercentage?: number;
18
+ private mouseUpEventHandler!: (event: MouseEvent) => void;
19
+ private mouseMoveEventHandler!: (event: MouseEvent) => void;
20
+ private min = 0;
21
+ private max = 100;
22
+
23
+ connectedCallback(): void {
24
+ super.connectedCallback();
25
+
26
+ this.mouseUpEventHandler = this.onMouseUp.bind(this);
27
+ this.mouseMoveEventHandler = this.onMouseMove.bind(this);
28
+
29
+ document.addEventListener('mouseup', this.mouseUpEventHandler);
30
+ document.addEventListener('mousemove', this.mouseMoveEventHandler);
31
+ }
32
+
33
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
34
+ switch (name) {
35
+ case 'min':
36
+ this.setMin(newValue);
37
+ break;
38
+ case 'max':
39
+ this.setMax(newValue);
40
+ break;
41
+ }
42
+
43
+ this.renderContent();
44
+ }
45
+
46
+ disconnectedCallback(): void {
47
+ super.disconnectedCallback();
48
+
49
+ document.removeEventListener('mouseup', this.mouseUpEventHandler);
50
+ document.removeEventListener('mousemove', this.mouseMoveEventHandler);
51
+ }
52
+
53
+ private setMin(value: string | null): void {
54
+ let min = parseInt(value ?? '');
55
+
56
+ if (isNaN(min) || min < 0) {
57
+ min = 0;
58
+ }
59
+
60
+ this.min = min;
61
+ }
62
+
63
+ private setMax(value: string | null): void {
64
+ let max = parseInt(value ?? '');
65
+
66
+ if (isNaN(max) || max > 100) {
67
+ max = 100;
68
+ }
69
+
70
+ this.max = max;
71
+ }
72
+
73
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
74
+ const html = template({
75
+ start: this.min,
76
+ end: this.max
77
+ });
78
+
79
+ return html;
80
+ }
81
+
82
+ protected bindChildElements(): void {
83
+ this.shadowRoot?.querySelectorAll('[hawksearch-handle]').forEach((e) => {
84
+ e.addEventListener('mousedown', ((event: MouseEvent): void => {
85
+ const handle = e.getAttribute('hawksearch-handle');
86
+
87
+ if (!handle) {
88
+ return;
89
+ }
90
+
91
+ event.preventDefault();
92
+
93
+ const element = event.currentTarget as HTMLElement;
94
+
95
+ this.activeHandle = element;
96
+ this.startOffsetPercentage = parseFloat(element.style.left ?? 0);
97
+ this.startX = event.clientX;
98
+ this.dragging = true;
99
+
100
+ // Ensure the clicked handle is always on top in stacking context
101
+ this.shadowRoot?.querySelectorAll('[hawksearch-handle]').forEach((h) => ((h as HTMLElement).style.zIndex = ''));
102
+
103
+ element.style.zIndex = '1';
104
+ }) as EventListener);
105
+ });
106
+ }
107
+
108
+ private onMouseUp(event: MouseEvent): void {
109
+ if (!this.dragging) {
110
+ return;
111
+ }
112
+
113
+ event.preventDefault();
114
+
115
+ this.dragging = false;
116
+
117
+ // Dispatch event
118
+ const data: RangeSliderEventData = {
119
+ min: this.min,
120
+ max: this.max
121
+ };
122
+
123
+ const newEvent = new CustomEvent('hawksearch:range-slider-changed', {
124
+ detail: data
125
+ });
126
+
127
+ this.dispatchEvent(newEvent);
128
+ }
129
+
130
+ private onMouseMove(event: MouseEvent): void {
131
+ if (!this.dragging || !this.activeHandle || this.startX === undefined) {
132
+ return;
133
+ }
134
+
135
+ const selectedHandle = this.activeHandle.getAttribute('hawksearch-handle');
136
+ const minHandle = this.shadowRoot?.querySelector('[hawksearch-handle="min"]') as HTMLElement;
137
+ const maxHandle = this.shadowRoot?.querySelector('[hawksearch-handle="max"]') as HTMLElement;
138
+ const bar = this.shadowRoot?.querySelector('[hawksearch-bar]') as HTMLElement;
139
+ const offset = event.clientX - this.startX;
140
+ const width = this.activeHandle.parentElement!.offsetWidth;
141
+
142
+ let offsetPercentage = Math.round((offset / width) * 100) + this.startOffsetPercentage!;
143
+ let min = 0;
144
+ let max = 100;
145
+
146
+ // Ensure that handles are never dragged beyond boundaries or having min > max
147
+ if (selectedHandle === 'min') {
148
+ max = parseFloat(maxHandle.style.left ?? 100);
149
+ } else if (selectedHandle === 'max') {
150
+ min = parseFloat(minHandle.style.left ?? 0);
151
+ }
152
+
153
+ if (offsetPercentage <= min) {
154
+ offsetPercentage = min;
155
+ } else if (offsetPercentage >= max) {
156
+ offsetPercentage = max;
157
+ }
158
+
159
+ // Update min and max to new values
160
+ if (selectedHandle === 'min') {
161
+ min = offsetPercentage;
162
+ } else if (selectedHandle === 'max') {
163
+ max = offsetPercentage;
164
+ }
165
+
166
+ // Set CSS properties
167
+ this.activeHandle.style.left = `${offsetPercentage}%`;
168
+ bar.style.left = `${min}%`;
169
+ bar.style.right = `${100 - max}%`;
170
+
171
+ // Update component properties
172
+ if (selectedHandle === 'min') {
173
+ this.min = offsetPercentage;
174
+ } else if (selectedHandle === 'max') {
175
+ this.max = offsetPercentage;
176
+ }
177
+
178
+ // Dispatch event
179
+ const data: RangeSliderEventData = {
180
+ min: this.min,
181
+ max: this.max
182
+ };
183
+
184
+ const newEvent = new CustomEvent('hawksearch:range-slider-dragging', {
185
+ detail: data
186
+ });
187
+
188
+ this.dispatchEvent(newEvent);
189
+ }
190
+ }
@@ -0,0 +1,4 @@
1
+ <div class='search'>
2
+ <input type='text' hawksearch-search-input value='{{query}}' placeholder='Enter keywords' />
3
+ <hawksearch-autocomplete></hawksearch-autocomplete>
4
+ </div>
@@ -0,0 +1,93 @@
1
+ import { debounce } from 'lodash';
2
+ import { SearchResponse } from '@models';
3
+ import { autocompleteService, searchService } from '@services';
4
+ import { AutocompleteComponent } from '../autocomplete/autocomplete.component';
5
+ import { BaseComponent } from '../base.component';
6
+ import defaultHtml from './search.component.hbs';
7
+
8
+ export class SearchComponent extends BaseComponent<SearchResponse> {
9
+ protected bindEvent = 'hawksearch:bind-search';
10
+ protected customTemplatePropertyName = 'search';
11
+ protected defaultHtml = defaultHtml;
12
+
13
+ private clickEventHandler!: (event: MouseEvent) => void;
14
+
15
+ connectedCallback(): void {
16
+ super.connectedCallback();
17
+
18
+ this.clickEventHandler = (event: Event): void => {
19
+ const element = event.target as HTMLElement;
20
+ const searchElement = !!element.closest('hawksearch-search');
21
+
22
+ if (!searchElement) {
23
+ this.toggleAutocomplete(false);
24
+ }
25
+ };
26
+
27
+ this.shadowRoot?.ownerDocument.addEventListener('click', this.clickEventHandler);
28
+ }
29
+
30
+ disconnectedCallback(): void {
31
+ super.disconnectedCallback();
32
+
33
+ this.shadowRoot?.ownerDocument.removeEventListener('click', this.connectedCallback);
34
+ }
35
+
36
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
37
+ const html = template({
38
+ ...this.data
39
+ });
40
+
41
+ return html;
42
+ }
43
+
44
+ protected bindChildElements(): void {
45
+ const enterKeys = ['Enter', 'NumpadEnter'];
46
+
47
+ this.shadowRoot?.querySelectorAll('[hawksearch-search-input]').forEach((e) => {
48
+ e.addEventListener('keyup', ((event: KeyboardEvent): void => {
49
+ if (!enterKeys.includes(event.code)) {
50
+ return;
51
+ }
52
+
53
+ const element = event.currentTarget as HTMLInputElement;
54
+ const query = element.value.trim();
55
+
56
+ this.toggleAutocomplete(false);
57
+
58
+ searchService.query(query);
59
+ }) as EventListener);
60
+
61
+ const onKeyUp = (event: KeyboardEvent): void => {
62
+ if (enterKeys.includes(event.code)) {
63
+ return;
64
+ }
65
+
66
+ const element = e as HTMLInputElement; // Not using event.currentTarget as that gets lost when using lodash debounce
67
+ const query = element.value.trim();
68
+
69
+ this.toggleAutocomplete(!!query);
70
+
71
+ if (query) {
72
+ autocompleteService.query(query);
73
+ }
74
+ };
75
+
76
+ e.addEventListener('keyup', debounce(onKeyUp, 300) as unknown as EventListener);
77
+ });
78
+
79
+ this.shadowRoot?.querySelectorAll('hawksearch-autocomplete').forEach((e) => {
80
+ e.addEventListener('hawksearch:close-autocomplete', (event: Event) => {
81
+ this.toggleAutocomplete(false);
82
+ });
83
+ });
84
+ }
85
+
86
+ private toggleAutocomplete(visible: boolean): void {
87
+ this.shadowRoot?.querySelectorAll('hawksearch-autocomplete').forEach((e) => {
88
+ const component = e as AutocompleteComponent;
89
+
90
+ component.style.display = visible ? 'block' : 'none';
91
+ });
92
+ }
93
+ }
@@ -0,0 +1,29 @@
1
+ <hawksearch-modified-query></hawksearch-modified-query>
2
+ <hawksearch-query-suggestions></hawksearch-query-suggestions>
3
+ <div class='row'>
4
+ <div class='column column--12 column-md--4'>
5
+ <hawksearch-facets-list></hawksearch-facets-list>
6
+ </div>
7
+ <div class='column column--12 column-md--8'>
8
+ <hawksearch-tabs></hawksearch-tabs>
9
+ <div class='margin'>
10
+ <hawksearch-selected-facets></hawksearch-selected-facets>
11
+ </div>
12
+ <div class='row'>
13
+ <div class='column column--12 column-sm--6 flex-vertical-sm-center'>
14
+ <hawksearch-pagination></hawksearch-pagination>
15
+ </div>
16
+ <div class='column column--12 column-md--6'>
17
+ <div class='row row--tight'>
18
+ <div class='column column--12 column-sm--6 flex-vertical-sm-center'>
19
+ <hawksearch-page-size></hawksearch-page-size>
20
+ </div>
21
+ <div class='column column--12 column-sm--6 flex-vertical-sm-center'>
22
+ <hawksearch-sorting></hawksearch-sorting>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <hawksearch-results-list></hawksearch-results-list>
28
+ </div>
29
+ </div>
@@ -0,0 +1,17 @@
1
+ import { SearchResponse } from '@models';
2
+ import { BaseComponent } from '../base.component';
3
+ import defaultHtml from './search-results.component.hbs';
4
+
5
+ export class SearchResultsComponent extends BaseComponent<SearchResponse> {
6
+ protected bindEvent = 'hawksearch:bind-search-results';
7
+ protected customTemplatePropertyName = 'searchResults';
8
+ protected defaultHtml = defaultHtml;
9
+
10
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
11
+ const html = template({
12
+ data: this.data
13
+ });
14
+
15
+ return html;
16
+ }
17
+ }
@@ -0,0 +1,23 @@
1
+ <div class='search-results-item search-results-item--{{type}}'>
2
+ {{#if (lt salePrice price)}}
3
+ <span class='search-results-item__sale-indicator'>Sale</span>
4
+ {{/if}}
5
+ {{#if (eq type 'product')}}
6
+ <a hawksearch-link href='{{url}}' class='search-results-item__image'>
7
+ <img hawksearch-image src='{{imageUrl}}' alt='' />
8
+ </a>
9
+ {{/if}}
10
+ <div class='search-results-item__title'>
11
+ <a hawksearch-link href='{{url}}'>{{title}}</a>
12
+ </div>
13
+ {{#unless (eq salePrice undefined)}}
14
+ <div class='search-results-item__price'>
15
+ {{#if (lt salePrice price)}}
16
+ <span class='search-results-item__price__original'>{{currency price 0}}</span>
17
+ <span class='search-results-item__price__current'>{{currency salePrice 0}}</span>
18
+ {{else}}
19
+ <span class='search-results-item__price-__current'>{{currency salePrice 0}}</span>
20
+ {{/if}}
21
+ </div>
22
+ {{/unless}}
23
+ </div>
@@ -0,0 +1,99 @@
1
+ import placeholderUrl from '@assets/images/placeholder.png';
2
+ import { formatCurrency } from '@helpers';
3
+ import { HawksearchGlobal, SearchResultsItem } from '@models';
4
+ import { trackingService } from '@services';
5
+ import { BaseComponent } from '../base.component';
6
+ import defaultHtml from './search-results-item.component.hbs';
7
+
8
+ declare let Hawksearch: HawksearchGlobal;
9
+
10
+ export class SearchResultsItemComponent extends BaseComponent<SearchResultsItem> {
11
+ protected bindEvent = undefined;
12
+ protected customTemplatePropertyName = 'searchResultsItem';
13
+ protected defaultHtml = defaultHtml;
14
+
15
+ protected registerHelpers(): void {
16
+ super.registerHelpers();
17
+
18
+ this.handlebars.registerHelper('string', (values: string | Array<string> | undefined, defaultValue?: string): string | undefined => {
19
+ if (typeof values === 'string') {
20
+ values = [values];
21
+ }
22
+
23
+ return this.getString(values, defaultValue);
24
+ });
25
+
26
+ this.handlebars.registerHelper('number', (values: string | Array<string> | undefined, defaultValue?: number): number | undefined => {
27
+ if (typeof values === 'string') {
28
+ values = [values];
29
+ }
30
+
31
+ return this.getNumber(values, defaultValue);
32
+ });
33
+
34
+ this.handlebars.registerHelper('currency', (values: number | Array<string> | undefined, defaultValue?: number): string | undefined => {
35
+ if (typeof values === 'string') {
36
+ values = [values];
37
+ } else if (typeof values === 'number') {
38
+ values = [values.toString()];
39
+ }
40
+
41
+ return this.getCurrency(values, defaultValue);
42
+ });
43
+ }
44
+
45
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
46
+ if (!this.data) {
47
+ return '';
48
+ }
49
+
50
+ const html = template(this.data);
51
+
52
+ return html;
53
+ }
54
+
55
+ protected bindChildElements(): void {
56
+ this.shadowRoot?.querySelectorAll('[hawksearch-image]').forEach((e) => {
57
+ e.addEventListener('error', (event: Event): void => {
58
+ const element = event.currentTarget as HTMLImageElement;
59
+
60
+ element.src = Hawksearch.config.placeholderImageUrl || placeholderUrl;
61
+ });
62
+ });
63
+
64
+ this.shadowRoot?.querySelectorAll('[hawksearch-link]').forEach((e) => {
65
+ e.addEventListener('click', ((event: PointerEvent): void => {
66
+ if (!this.data) {
67
+ return;
68
+ }
69
+
70
+ trackingService.trackSearchResultClick(this.data.id, this.data.url, event);
71
+ }) as EventListener);
72
+ });
73
+ }
74
+
75
+ private getString(values: Array<string> | undefined, defaultValue?: string): string | undefined {
76
+ return values?.[0] ?? defaultValue;
77
+ }
78
+
79
+ private getNumber(values: Array<string> | undefined, defaultValue?: number): number | undefined {
80
+ const string = this.getString(values);
81
+ let value = string ? parseFloat(string) : undefined;
82
+
83
+ if (value !== undefined && isNaN(value)) {
84
+ value = undefined;
85
+ }
86
+
87
+ return value ?? defaultValue;
88
+ }
89
+
90
+ private getCurrency(values: Array<string> | undefined, defaultValue?: number): string | undefined {
91
+ const value = this.getNumber(values) || defaultValue;
92
+
93
+ if (!value) {
94
+ return undefined;
95
+ }
96
+
97
+ return formatCurrency(value);
98
+ }
99
+ }
@@ -0,0 +1,15 @@
1
+ {{#if items.length}}
2
+ <div class='row'>
3
+ {{#each items}}
4
+ {{#if (eq type 'content')}}
5
+ <div class='column column--12'>
6
+ <hawksearch-results-item></hawksearch-results-item>
7
+ </div>
8
+ {{else}}
9
+ <div class='column column--12 column-sm--6 column-lg--4'>
10
+ <hawksearch-results-item></hawksearch-results-item>
11
+ </div>
12
+ {{/if}}
13
+ {{/each}}
14
+ </div>
15
+ {{/if}}
@@ -0,0 +1,24 @@
1
+ import { SearchResultsItem } from '@models';
2
+ import { BaseComponent } from '../base.component';
3
+ import { SearchResultsItemComponent } from '../search-results-item/search-results-item.component';
4
+ import defaultHtml from './search-results-list.component.hbs';
5
+
6
+ export class SearchResultsListComponent extends BaseComponent<Array<SearchResultsItem>> {
7
+ protected bindEvent = 'hawksearch:bind-search-results-list';
8
+ protected customTemplatePropertyName = 'searchResultsList';
9
+ protected defaultHtml = defaultHtml;
10
+
11
+ protected getContentHtml(template: HandlebarsTemplateDelegate): string {
12
+ const html = template({
13
+ items: this.data ?? []
14
+ });
15
+
16
+ return html;
17
+ }
18
+
19
+ protected bindChildElements(): void {
20
+ this.shadowRoot?.querySelectorAll<SearchResultsItemComponent>('hawksearch-results-item').forEach((component, index) => {
21
+ component.data = this.data?.[index];
22
+ });
23
+ }
24
+ }
@@ -0,0 +1,11 @@
1
+ <div class='flex-gap flex-gap--xs'>
2
+ {{#each selections}}
3
+ <span class='selected-facet'>
4
+ <span class='selected-facet__field'>{{title}}</span>
5
+ <span class='selected-facet__value{{attribute ' selected-facet__value--excluded' excluded}}'>{{selectionTitle}}</span>
6
+ <a hawksearch-facet-field='{{field}}' hawksearch-facet-value='{{selectionValue}}' class='selected-facet__remove'>
7
+ <hawksearch-icon name='cross' size='0.9em'></hawksearch-icon>
8
+ </a>
9
+ </span>
10
+ {{/each}}
11
+ </div>