@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,754 @@
1
+ import { cloneDeep } from 'lodash';
2
+ import {
3
+ FacetType,
4
+ FacetValue,
5
+ FacetValueColor,
6
+ HawksearchGlobal,
7
+ HawksearchState,
8
+ ModifiedQueryData,
9
+ QuerySuggestionsData,
10
+ RawFacet,
11
+ RawFacetValue,
12
+ RawSearchRequest,
13
+ RawSearchResponse,
14
+ RawSearchSelection,
15
+ SearchRequest,
16
+ SearchResponse,
17
+ SelectedFacet,
18
+ SelectedFacets
19
+ } from '../models';
20
+ import { BaseService } from './base.service';
21
+ import { trackingService } from './tracking.service';
22
+
23
+ declare let Hawksearch: HawksearchGlobal;
24
+
25
+ export class SearchService extends BaseService {
26
+ protected baseUrl = Hawksearch.config.endpoints?.search || 'https://searchapi-dev.hawksearch.net/api/v2';
27
+
28
+ // #region Search Operations
29
+
30
+ async search(searchRequest: SearchRequest): Promise<SearchResponse> {
31
+ const searchReults = await this.executeSearch(searchRequest, true, false);
32
+
33
+ this.bindComponents(searchReults);
34
+
35
+ return searchReults;
36
+ }
37
+
38
+ async query(query?: string, disableSpellcheck = false): Promise<SearchResponse> {
39
+ const request: SearchRequest = {
40
+ disableSpellcheck: !!disableSpellcheck || undefined,
41
+ query: query,
42
+ newRequest: true
43
+ };
44
+
45
+ const searchResponse = await this.executeSearch(request, true, false);
46
+
47
+ this.bindComponents(searchResponse);
48
+
49
+ return searchResponse;
50
+ }
51
+
52
+ async addFacetValue(field: string, value: string): Promise<SearchResponse> {
53
+ this.ensureSearchRequest();
54
+
55
+ const searchRequest = cloneDeep(Hawksearch.searchRequest!);
56
+
57
+ searchRequest.newRequest = false;
58
+ searchRequest.page = undefined;
59
+
60
+ if (!searchRequest.facets) {
61
+ searchRequest.facets = {};
62
+ }
63
+
64
+ this.clearExistingFacetValue(searchRequest, field, value);
65
+
66
+ searchRequest.facets[field] = [...(searchRequest.facets[field]?.filter((v) => v !== value) ?? []), value];
67
+
68
+ const searchResponse = await this.executeSearch(searchRequest, false);
69
+
70
+ this.bindFacetsListComponent(searchResponse);
71
+ this.bindPaginationComponent(searchResponse);
72
+ this.bindSelectedFacetsComponent(searchResponse);
73
+ this.bindTabsComponent(searchResponse);
74
+
75
+ return searchResponse;
76
+ }
77
+
78
+ async removeFacetValue(field: string, value: string): Promise<SearchResponse> {
79
+ this.ensureSearchRequest();
80
+
81
+ if (field === 'searchWithin') {
82
+ return await this.searchWithin('');
83
+ }
84
+
85
+ const searchRequest = cloneDeep(Hawksearch.searchRequest!);
86
+
87
+ searchRequest.newRequest = false;
88
+ searchRequest.page = undefined;
89
+
90
+ this.clearExistingFacetValue(searchRequest, field, value);
91
+
92
+ const searchResponse = await this.executeSearch(searchRequest, false);
93
+
94
+ this.bindFacetsListComponent(searchResponse);
95
+ this.bindPaginationComponent(searchResponse);
96
+ this.bindSelectedFacetsComponent(searchResponse);
97
+ this.bindTabsComponent(searchResponse);
98
+
99
+ return searchResponse;
100
+ }
101
+
102
+ includeFacetValue(field: string, value: string): Promise<SearchResponse> {
103
+ const excludedValue = `@${value}`;
104
+
105
+ return this.removeFacetValue(field, excludedValue);
106
+ }
107
+
108
+ async excludeFacetValue(field: string, value: string): Promise<SearchResponse> {
109
+ this.ensureSearchRequest();
110
+
111
+ const excludedValue = `@${value}`;
112
+ const searchRequest = cloneDeep(Hawksearch.searchRequest!);
113
+
114
+ searchRequest.newRequest = false;
115
+ searchRequest.page = undefined;
116
+
117
+ if (!searchRequest.facets) {
118
+ searchRequest.facets = {};
119
+ }
120
+
121
+ this.clearExistingFacetValue(searchRequest, field, value);
122
+
123
+ searchRequest.facets = {
124
+ ...searchRequest.facets,
125
+ [field]: [...(searchRequest.facets[field] ?? []), excludedValue]
126
+ };
127
+
128
+ const searchResponse = await this.executeSearch(searchRequest, false);
129
+
130
+ this.bindFacetsListComponent(searchResponse);
131
+ this.bindPaginationComponent(searchResponse);
132
+ this.bindSelectedFacetsComponent(searchResponse);
133
+ this.bindTabsComponent(searchResponse);
134
+
135
+ return searchResponse;
136
+ }
137
+
138
+ private clearExistingFacetValue(searchRequest: SearchRequest, field: string, value: string): void {
139
+ value = value.replace(/^@/, '');
140
+
141
+ const excludedValue = `@${value}`;
142
+
143
+ if (!searchRequest.facets) {
144
+ searchRequest.facets = {};
145
+ }
146
+
147
+ searchRequest.facets = {
148
+ ...searchRequest.facets,
149
+ [field]: [...(searchRequest.facets[field]?.filter((v) => v !== value && v !== excludedValue) ?? [])]
150
+ };
151
+
152
+ if (!searchRequest.facets[field]?.length) {
153
+ delete searchRequest.facets[field];
154
+ }
155
+ }
156
+
157
+ async setFacetValue(field: string, value: string): Promise<SearchResponse> {
158
+ this.ensureSearchRequest();
159
+
160
+ const request = cloneDeep(Hawksearch.searchRequest!);
161
+
162
+ request.newRequest = false;
163
+ request.page = undefined;
164
+
165
+ if (!request.facets) {
166
+ request.facets = {};
167
+ }
168
+
169
+ request.facets[field] = [value];
170
+
171
+ const searchResponse = await this.executeSearch(request, false);
172
+
173
+ this.bindFacetsListComponent(searchResponse);
174
+ this.bindPaginationComponent(searchResponse);
175
+ this.bindSelectedFacetsComponent(searchResponse);
176
+ this.bindTabsComponent(searchResponse);
177
+
178
+ return searchResponse;
179
+ }
180
+
181
+ async modifySearch(query: string | undefined, facets: SelectedFacets | undefined): Promise<SearchResponse> {
182
+ this.ensureSearchRequest();
183
+
184
+ const request = cloneDeep(Hawksearch.searchRequest!);
185
+
186
+ request.query = query;
187
+ request.facets = facets;
188
+ request.newRequest = false;
189
+ request.page = undefined;
190
+
191
+ const searchResponse = await this.executeSearch(request, false);
192
+
193
+ this.bindFacetsListComponent(searchResponse);
194
+ this.bindPaginationComponent(searchResponse);
195
+ this.bindSelectedFacetsComponent(searchResponse);
196
+ this.bindTabsComponent(searchResponse);
197
+
198
+ return searchResponse;
199
+ }
200
+
201
+ async searchWithin(value: string): Promise<SearchResponse> {
202
+ this.ensureSearchRequest();
203
+
204
+ const request = cloneDeep(Hawksearch.searchRequest!);
205
+
206
+ request.newRequest = false;
207
+ request.page = undefined;
208
+ request.searchWithin = value || undefined;
209
+
210
+ const searchResponse = await this.executeSearch(request, false);
211
+
212
+ this.bindFacetsListComponent(searchResponse);
213
+ this.bindPaginationComponent(searchResponse);
214
+ this.bindSelectedFacetsComponent(searchResponse);
215
+ this.bindTabsComponent(searchResponse);
216
+
217
+ return searchResponse;
218
+ }
219
+
220
+ async selectTab(value?: string): Promise<SearchResponse> {
221
+ this.ensureSearchRequest();
222
+
223
+ const request = cloneDeep(Hawksearch.searchRequest!);
224
+
225
+ request.newRequest = false;
226
+ request.page = undefined;
227
+
228
+ if (!request.facets) {
229
+ request.facets = {};
230
+ }
231
+
232
+ const field = Hawksearch.searchResponse?.facets?.find((f) => f.type === 'tabs')?.field;
233
+
234
+ if (field) {
235
+ if (value) {
236
+ request.facets[field] = [value];
237
+ } else {
238
+ delete request.facets[field];
239
+ }
240
+ }
241
+
242
+ const searchResponse = await this.executeSearch(request, false);
243
+
244
+ this.bindFacetsListComponent(searchResponse);
245
+ this.bindPaginationComponent(searchResponse);
246
+ this.bindSelectedFacetsComponent(searchResponse);
247
+ this.bindTabsComponent(searchResponse);
248
+
249
+ return searchResponse;
250
+ }
251
+
252
+ async paginate(page: number): Promise<SearchResponse> {
253
+ this.ensureSearchRequest();
254
+
255
+ const request = cloneDeep(Hawksearch.searchRequest!);
256
+
257
+ request.newRequest = false;
258
+ request.page = page;
259
+
260
+ const searchResponse = await this.executeSearch(request, false);
261
+
262
+ this.bindPaginationComponent(searchResponse);
263
+
264
+ return searchResponse;
265
+ }
266
+
267
+ async setPageSize(pageSize: number): Promise<SearchResponse> {
268
+ this.ensureSearchRequest();
269
+
270
+ const request = cloneDeep(Hawksearch.searchRequest!);
271
+
272
+ request.newRequest = false;
273
+ request.pageSize = pageSize;
274
+
275
+ const searchResponse = await this.executeSearch(request, false);
276
+
277
+ this.bindPageSizeComponent(searchResponse);
278
+ this.bindPaginationComponent(searchResponse);
279
+
280
+ return searchResponse;
281
+ }
282
+
283
+ async sort(value?: string): Promise<SearchResponse> {
284
+ this.ensureSearchRequest();
285
+
286
+ const request = cloneDeep(Hawksearch.searchRequest!);
287
+
288
+ request.newRequest = false;
289
+ request.page = 1;
290
+ request.sort = value;
291
+
292
+ const searchResponse = await this.executeSearch(request, false);
293
+
294
+ this.bindPaginationComponent(searchResponse);
295
+ this.bindSortingComponent(searchResponse);
296
+
297
+ return searchResponse;
298
+ }
299
+
300
+ private executeSearch(searchRequest: SearchRequest, newSearch: boolean, executeDefaultBindings = true): Promise<SearchResponse> {
301
+ Hawksearch.searchRequest = searchRequest;
302
+
303
+ const rawRequest: RawSearchRequest = {
304
+ ClientData: {
305
+ UserAgent: window.navigator.userAgent,
306
+ VisitorId: this.getVisitorId(),
307
+ VisitId: this.getVisitId()
308
+ },
309
+ ClientGuid: Hawksearch.config.clientId,
310
+ IgnoreSpellcheck: searchRequest.disableSpellcheck,
311
+ Keyword: searchRequest.query,
312
+ FacetSelections: searchRequest.facets,
313
+ MaxPerPage: searchRequest.pageSize,
314
+ PageNo: searchRequest.page,
315
+ SearchWithin: searchRequest.searchWithin,
316
+ SortBy: searchRequest.sort
317
+ };
318
+
319
+ return this.httpPost<RawSearchResponse>('/search', rawRequest).then((rawSearchResponse) => {
320
+ const searchResponse = this.convertSearchResponse(searchRequest, rawSearchResponse);
321
+
322
+ Hawksearch.searchResponse = searchResponse;
323
+
324
+ trackingService.trackSearch(searchRequest.query, newSearch);
325
+
326
+ this.setQueryString();
327
+
328
+ const newEvent = new CustomEvent('hawksearch:search-completed', {
329
+ detail: searchResponse
330
+ });
331
+
332
+ dispatchEvent(newEvent);
333
+
334
+ if (executeDefaultBindings) {
335
+ this.bindModifiedQueryComponent(searchResponse);
336
+ this.bindSearchComponent(searchResponse);
337
+ this.bindSearchResultsListComponent(searchResponse);
338
+ this.bindQuerySuggestionsComponent(searchResponse);
339
+ }
340
+
341
+ if (newSearch && searchResponse.redirect) {
342
+ window.open(searchResponse.redirect.url, searchResponse.redirect.target ?? '_self');
343
+ }
344
+
345
+ return searchResponse;
346
+ });
347
+ }
348
+
349
+ private ensureSearchRequest(): void {
350
+ if (!Hawksearch.searchRequest) {
351
+ throw new Error('You cannot refine search results until an initial search has been executed');
352
+ }
353
+ }
354
+
355
+ // #endregion Search Operations
356
+
357
+ // #region Model Binding
358
+
359
+ private convertSearchResponse(searchRequest: SearchRequest, searchResponse: RawSearchResponse): SearchResponse {
360
+ const getFacetType = (facet: RawFacet): FacetType => {
361
+ if (facet.FieldType === 'tab') {
362
+ return 'tabs';
363
+ }
364
+
365
+ switch (facet.FacetType) {
366
+ case 'nestedcheckbox':
367
+ return 'checkbox';
368
+ case 'slider':
369
+ return 'range-slider';
370
+ case 'swatch':
371
+ return 'color';
372
+ default:
373
+ return facet.FacetType as FacetType;
374
+ }
375
+ };
376
+
377
+ const getFacetValueColor = (facet: RawFacet, value: RawFacetValue): FacetValueColor | undefined => {
378
+ const swatchValue = facet.SwatchData?.find((v) => v.Value.toLowerCase() === value.Value.toLowerCase());
379
+
380
+ if (!swatchValue) {
381
+ return undefined;
382
+ }
383
+
384
+ return {
385
+ name: swatchValue.Value,
386
+ hex: swatchValue.Color || swatchValue.Value,
387
+ imageUrl: this.getUrl(Hawksearch.config.urlPrefixes?.assets, swatchValue.AssetUrl || swatchValue.AssetName)
388
+ };
389
+ };
390
+
391
+ const getFacetValues = (
392
+ facet: RawFacet,
393
+ values: Array<RawFacetValue> | undefined,
394
+ selection: RawSearchSelection | undefined
395
+ ): Array<FacetValue> | undefined => {
396
+ return values?.map((v) => ({
397
+ title: v.Label,
398
+ value: v.Value,
399
+ color: getFacetValueColor(facet, v),
400
+ count: v.Count,
401
+ level: v.Level,
402
+ selected: v.Selected,
403
+ excluded: selection?.Items?.some((s) => s.Value === `@${v.Value}`),
404
+ children: getFacetValues(facet, v.Children, selection)
405
+ }));
406
+ };
407
+
408
+ const getSearchWithinSelectedFacets = (): Array<SelectedFacet> => {
409
+ const facetName = 'searchWithin';
410
+ const facet = searchResponse.Facets?.find((f) => f.Field === facetName);
411
+
412
+ if (!searchRequest.searchWithin || !facet) {
413
+ return [];
414
+ }
415
+
416
+ return [
417
+ {
418
+ field: facetName,
419
+ title: facet.Name,
420
+ values: [
421
+ {
422
+ title: searchRequest.searchWithin,
423
+ value: searchRequest.searchWithin,
424
+ excluded: false
425
+ }
426
+ ]
427
+ }
428
+ ];
429
+ };
430
+
431
+ return {
432
+ facets: searchResponse.Facets?.map((f) => ({
433
+ id: f.FacetId,
434
+ type: getFacetType(f),
435
+ title: f.Name,
436
+ field: f.ParamName || f.Field || undefined,
437
+ collapsible: f.IsCollapsible ?? false,
438
+ collapsed: f.IsCollapsedDefault,
439
+ rangeMin: getFacetType(f) === 'range-slider' && f.Values?.length ? parseFloat(f.Values[0].RangeMin ?? '0') : undefined,
440
+ rangeMax: getFacetType(f) === 'range-slider' && f.Values?.length ? parseFloat(f.Values[0].RangeMax ?? '0') : undefined,
441
+ rangeStart: getFacetType(f) === 'range-slider' && f.Values?.length ? parseFloat(f.Values[0].RangeStart ?? '0') : undefined,
442
+ rangeEnd: getFacetType(f) === 'range-slider' && f.Values?.length ? parseFloat(f.Values[0].RangeEnd ?? '0') : undefined,
443
+ searchable: f.IsSearch,
444
+ searchWithin: searchRequest.searchWithin,
445
+ tooltip: this.stripHtml(f.Tooltip ?? '') || undefined,
446
+ truncateThreshold: parseFloat(f.TruncateThreshold as any) || undefined,
447
+ values: getFacetValues(f, f.Values, searchResponse.Selections?.[f.ParamName || f.Field])
448
+ })),
449
+ modifiedQuery: searchResponse.AdjustedKeyword,
450
+ pagination: searchResponse.Pagination
451
+ ? {
452
+ page: searchResponse.Pagination.CurrentPage,
453
+ pageSize: searchResponse.Pagination.MaxPerPage,
454
+ totalPages: searchResponse.Pagination.NofPages,
455
+ totalResults: searchResponse.Pagination.NofResults,
456
+ maxPageLinks: searchResponse.Pagination.NumberOfPageLinks > 0 ? searchResponse.Pagination.NumberOfPageLinks : undefined,
457
+ displayFirstLink: searchResponse.Pagination.IsShowFirstLink,
458
+ displayLastLink: searchResponse.Pagination.IsShowLastLink,
459
+ options:
460
+ searchResponse.Pagination.Items.map((i) => ({
461
+ title: i.Label,
462
+ pageSize: i.PageSize,
463
+ selected: i.Selected,
464
+ default: i.Default
465
+ })) ?? []
466
+ }
467
+ : undefined,
468
+ query: searchResponse.Keyword,
469
+ querySuggestions: searchResponse.DidYouMean,
470
+ redirect: searchResponse.Redirect
471
+ ? {
472
+ url: searchResponse.Redirect.Location,
473
+ target: searchResponse.Redirect.Target
474
+ }
475
+ : undefined,
476
+ results: searchResponse.Results?.map((r) => ({
477
+ id: r.DocId,
478
+ pinned: r.IsPin ?? false,
479
+ visible: r.IsVisible ?? true,
480
+ score: r.Score,
481
+ type: r.Document?.['contenttype']?.[0] === 'Content' ? 'content' : 'product',
482
+ imageUrl: this.getUrl(Hawksearch.config.urlPrefixes?.content, this.getString(r.Document?.[this.fieldMappings.imageUrl])),
483
+ title: this.getString(r.Document?.[this.fieldMappings.title])!,
484
+ url: this.getUrl(Hawksearch.config.urlPrefixes?.content, this.getString(r.Document?.[this.fieldMappings.url]))!,
485
+ price: this.getNumber(r.Document?.[this.fieldMappings.price]),
486
+ salePrice: this.getNumber(r.Document?.[this.fieldMappings.salePrice]),
487
+ attributes: r.Document
488
+ })),
489
+ selectedFacets: [
490
+ ...Object.keys(searchResponse.Selections ?? []).map((field) => {
491
+ const selection = searchResponse.Selections![field];
492
+ const excludedRegex = new RegExp('^@');
493
+
494
+ return {
495
+ field: field,
496
+ title: selection.Label,
497
+ values: selection.Items.map((i) => ({
498
+ title: i.Label.replace(excludedRegex, ''),
499
+ value: i.Value,
500
+ excluded: excludedRegex.test(i.Value)
501
+ }))
502
+ };
503
+ }),
504
+ ...getSearchWithinSelectedFacets()
505
+ ],
506
+ success: searchResponse.Success,
507
+ sorting: {
508
+ value: searchResponse.Sorting?.Value,
509
+ options:
510
+ searchResponse.Sorting?.Items.map((i) => ({
511
+ title: i.Label,
512
+ value: i.Value,
513
+ selected: i.Selected,
514
+ default: i.IsDefault
515
+ })) ?? []
516
+ },
517
+ trackingId: searchResponse.TrackingId
518
+ };
519
+ }
520
+
521
+ private getString(values: Array<string> | undefined, defaultValue?: string): string | undefined {
522
+ return values?.[0] ?? defaultValue;
523
+ }
524
+
525
+ private getNumber(values: Array<string> | undefined, defaultValue?: number): number | undefined {
526
+ const string = this.getString(values);
527
+ let value = string ? parseFloat(string) : undefined;
528
+
529
+ if (value !== undefined && isNaN(value)) {
530
+ value = undefined;
531
+ }
532
+
533
+ return value ?? defaultValue;
534
+ }
535
+
536
+ private getUrl(prefix: string | undefined, url: string | undefined): string | undefined {
537
+ if (!url) {
538
+ return undefined;
539
+ }
540
+
541
+ if (!!prefix && !/^https?:\/\//.test(url)) {
542
+ return `${prefix}/${url.replace(/^\/+/, '')}`;
543
+ }
544
+
545
+ return url;
546
+ }
547
+
548
+ // #endregion Model Binding
549
+
550
+ // #region Events
551
+
552
+ bindComponents(searchResponse: SearchResponse): void {
553
+ this.bindSearchResultsComponent(searchResponse); // This needs to be first since it contains the other components
554
+ this.bindFacetsListComponent(searchResponse);
555
+ this.bindModifiedQueryComponent(searchResponse);
556
+ this.bindPageSizeComponent(searchResponse);
557
+ this.bindPaginationComponent(searchResponse);
558
+ this.bindQuerySuggestionsComponent(searchResponse);
559
+ this.bindSearchComponent(searchResponse);
560
+ this.bindSearchResultsListComponent(searchResponse);
561
+ this.bindSelectedFacetsComponent(searchResponse);
562
+ this.bindSortingComponent(searchResponse);
563
+ this.bindTabsComponent(searchResponse);
564
+ }
565
+
566
+ bindFacetsListComponent(searchResponse: SearchResponse | undefined): void {
567
+ this.triggerEvent('hawksearch:bind-facets-list', searchResponse?.facets);
568
+ }
569
+
570
+ bindModifiedQueryComponent(searchResponse: SearchResponse | undefined): void {
571
+ const data: ModifiedQueryData = {
572
+ display: !!searchResponse?.query && !!searchResponse?.modifiedQuery && Hawksearch.searchRequest!.newRequest,
573
+ query: searchResponse?.query,
574
+ modifiedQuery: searchResponse?.modifiedQuery
575
+ };
576
+
577
+ this.triggerEvent('hawksearch:bind-modified-query', data);
578
+ }
579
+
580
+ bindPageSizeComponent(searchResponse: SearchResponse | undefined): void {
581
+ this.triggerEvent('hawksearch:bind-page-size', searchResponse?.pagination?.options);
582
+ }
583
+
584
+ bindPaginationComponent(searchResponse: SearchResponse | undefined): void {
585
+ this.triggerEvent('hawksearch:bind-pagination', searchResponse?.pagination);
586
+ }
587
+
588
+ bindQuerySuggestionsComponent(searchResponse: SearchResponse | undefined): void {
589
+ const data: QuerySuggestionsData = {
590
+ display: !!searchResponse?.querySuggestions?.length && Hawksearch.searchRequest!.newRequest,
591
+ querySuggestions: searchResponse?.querySuggestions
592
+ };
593
+
594
+ this.triggerEvent('hawksearch:bind-query-suggestions', data);
595
+ }
596
+
597
+ bindSortingComponent(searchResponse: SearchResponse | undefined): void {
598
+ this.triggerEvent('hawksearch:bind-sorting', searchResponse?.sorting);
599
+ }
600
+
601
+ bindSearchComponent(searchResponse: SearchResponse | undefined): void {
602
+ this.triggerEvent('hawksearch:bind-search', searchResponse);
603
+ }
604
+
605
+ bindSearchResultsComponent(searchResponse: SearchResponse | undefined): void {
606
+ this.triggerEvent('hawksearch:bind-search-results', searchResponse);
607
+ }
608
+
609
+ bindSearchResultsListComponent(searchResponse: SearchResponse | undefined): void {
610
+ this.triggerEvent('hawksearch:bind-search-results-list', searchResponse?.results);
611
+ }
612
+
613
+ bindSelectedFacetsComponent(searchResponse: SearchResponse | undefined): void {
614
+ this.triggerEvent('hawksearch:bind-selected-facets', searchResponse?.selectedFacets);
615
+ }
616
+
617
+ bindTabsComponent(searchResponse: SearchResponse | undefined): void {
618
+ this.triggerEvent(
619
+ 'hawksearch:bind-tabs',
620
+ searchResponse?.facets?.find((f) => f.type === 'tabs')
621
+ );
622
+ }
623
+
624
+ private triggerEvent<T>(name: string, data: T): void {
625
+ const event = new CustomEvent(name, { detail: data });
626
+
627
+ dispatchEvent(event);
628
+ }
629
+
630
+ // #endregion Events
631
+
632
+ // #region QueryString
633
+
634
+ getRequestFromUrl(): SearchRequest {
635
+ const url = new URL(window.location.href);
636
+ const facets: SelectedFacets = {};
637
+ const facetRegex = new RegExp(`^${this.queryStringParams.facet}-`);
638
+
639
+ for (const key of Array.from(url.searchParams.keys()).filter((k) => facetRegex.test(k))) {
640
+ const field = key.replace(facetRegex, '');
641
+
642
+ facets[field] = url.searchParams.getAll(key);
643
+ }
644
+
645
+ return {
646
+ disableSpellcheck: this.getBooleanParameter(url.searchParams.get(this.queryStringParams.disableSpellcheck)),
647
+ facets: facets,
648
+ newRequest: true,
649
+ pageSize: this.getPositiveIntegerParameter(url.searchParams.get(this.queryStringParams.pageSize)),
650
+ page: this.getPositiveIntegerParameter(url.searchParams.get(this.queryStringParams.page)),
651
+ query: this.getStringParameter(url.searchParams.get(this.queryStringParams.query)),
652
+ searchWithin: this.getStringParameter(url.searchParams.get(this.queryStringParams.searchWithin)),
653
+ sort: this.getStringParameter(url.searchParams.get(this.queryStringParams.sort))
654
+ };
655
+ }
656
+
657
+ private setQueryString(): void {
658
+ if (!Hawksearch.searchRequest) {
659
+ return;
660
+ }
661
+
662
+ const request = Hawksearch.searchRequest;
663
+ const url = new URL(window.location.href);
664
+
665
+ url.searchParams.delete(this.queryStringParams.query);
666
+
667
+ if (request.query) {
668
+ url.searchParams.set(this.queryStringParams.query, request.query);
669
+ }
670
+
671
+ for (const key of url.searchParams.keys()) {
672
+ if (key.startsWith(`${this.queryStringParams.facet}-`)) {
673
+ url.searchParams.delete(key);
674
+ }
675
+ }
676
+
677
+ if (request.disableSpellcheck) {
678
+ url.searchParams.set(this.queryStringParams.disableSpellcheck, request.disableSpellcheck.toString());
679
+ }
680
+
681
+ if (request.facets) {
682
+ Object.keys(request.facets).forEach((field) => {
683
+ const values = request.facets![field];
684
+
685
+ values?.forEach((value) => url.searchParams.append(`${this.queryStringParams.facet}-${field}`, value));
686
+ });
687
+ }
688
+
689
+ url.searchParams.delete(this.queryStringParams.searchWithin);
690
+
691
+ if (request.searchWithin) {
692
+ url.searchParams.set(this.queryStringParams.searchWithin, request.searchWithin);
693
+ }
694
+
695
+ url.searchParams.delete(this.queryStringParams.sort);
696
+
697
+ if (request.sort) {
698
+ url.searchParams.set(this.queryStringParams.sort, request.sort);
699
+ }
700
+
701
+ url.searchParams.delete(this.queryStringParams.pageSize);
702
+
703
+ if (request.pageSize) {
704
+ url.searchParams.set(this.queryStringParams.pageSize, request.pageSize.toString());
705
+ }
706
+
707
+ url.searchParams.delete(this.queryStringParams.page);
708
+
709
+ if (request.page) {
710
+ url.searchParams.set(this.queryStringParams.page, request.page.toString());
711
+ }
712
+
713
+ const state: HawksearchState = {
714
+ searchRequest: Hawksearch.searchRequest,
715
+ searchResponse: Hawksearch.searchResponse!
716
+ };
717
+
718
+ if (url.toString() === window.location.href) {
719
+ window.history.replaceState(state, '');
720
+ } else {
721
+ window.history.pushState(state, '', url.toString());
722
+ }
723
+ }
724
+
725
+ private getBooleanParameter(input: string | null): boolean | undefined {
726
+ if (input === null) {
727
+ return undefined;
728
+ }
729
+
730
+ return input === 'true';
731
+ }
732
+
733
+ private getPositiveIntegerParameter(input: string | null): number | undefined {
734
+ if (!input) {
735
+ return undefined;
736
+ }
737
+
738
+ const value = parseInt(input);
739
+
740
+ return value > 0 ? value : undefined;
741
+ }
742
+
743
+ private getStringParameter(input: string | null): string | undefined {
744
+ if (!input || !input.trim()) {
745
+ return undefined;
746
+ }
747
+
748
+ return input.trim();
749
+ }
750
+
751
+ // #endregion QueryString
752
+ }
753
+
754
+ export const searchService = new SearchService();