@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.
- package/.eslintignore +2 -0
- package/.eslintrc.json +153 -0
- package/.husky/pre-commit +5 -0
- package/.lintstagedrc.json +4 -0
- package/.prettierignore +7 -0
- package/.prettierrc.json +19 -0
- package/.vscode/launch.json +15 -0
- package/custom1.html +52 -0
- package/custom2.html +44 -0
- package/gulpfile.js +73 -0
- package/index.html +39 -0
- package/package.json +57 -0
- package/src/assets/icons/chevron-back.svg +4 -0
- package/src/assets/icons/chevron-down.svg +3 -0
- package/src/assets/icons/chevron-forward.svg +4 -0
- package/src/assets/icons/chevron-left.svg +3 -0
- package/src/assets/icons/chevron-right.svg +3 -0
- package/src/assets/icons/chevron-up.svg +3 -0
- package/src/assets/icons/cross.svg +3 -0
- package/src/assets/icons/help.svg +4 -0
- package/src/assets/icons/minus.svg +3 -0
- package/src/assets/icons/plus.svg +7 -0
- package/src/assets/images/icons.svg +1 -0
- package/src/assets/images/placeholder.png +0 -0
- package/src/components/autocomplete/autocomplete.component.hbs +60 -0
- package/src/components/autocomplete/autocomplete.component.ts +124 -0
- package/src/components/base.component.ts +120 -0
- package/src/components/facet-types/base-facet.component.ts +114 -0
- package/src/components/facet-types/checkbox-list-facet/checkbox-list-facet.component.hbs +6 -0
- package/src/components/facet-types/checkbox-list-facet/checkbox-list-facet.component.ts +73 -0
- package/src/components/facet-types/checkbox-list-facet/checkbox-list.partial.hbs +42 -0
- package/src/components/facet-types/color-facet/color-facet.component.hbs +29 -0
- package/src/components/facet-types/color-facet/color-facet.component.ts +26 -0
- package/src/components/facet-types/index.ts +6 -0
- package/src/components/facet-types/linked-list-facet/link-list-facet.component.hbs +17 -0
- package/src/components/facet-types/linked-list-facet/link-list-facet.component.ts +32 -0
- package/src/components/facet-types/range-slider-facet/range-slider-facet.component.hbs +11 -0
- package/src/components/facet-types/range-slider-facet/range-slider-facet.component.ts +107 -0
- package/src/components/facet-types/search-within-facet/search-within-facet.component.hbs +3 -0
- package/src/components/facet-types/search-within-facet/search-within-facet.component.ts +33 -0
- package/src/components/facet-types/size-facet/size-facet.component.hbs +29 -0
- package/src/components/facet-types/size-facet/size-facet.component.ts +30 -0
- package/src/components/facet-wrapper/facet-wrapper.component.hbs +36 -0
- package/src/components/facet-wrapper/facet-wrapper.component.ts +62 -0
- package/src/components/facets-list/facets-list.component.hbs +11 -0
- package/src/components/facets-list/facets-list.component.ts +92 -0
- package/src/components/icon/icon.component.hbs +3 -0
- package/src/components/icon/icon.component.ts +40 -0
- package/src/components/index.ts +18 -0
- package/src/components/modified-query/modified-query.component.hbs +7 -0
- package/src/components/modified-query/modified-query.component.ts +38 -0
- package/src/components/page-size/page-size.component.hbs +7 -0
- package/src/components/page-size/page-size.component.ts +35 -0
- package/src/components/pagination/pagination.component.hbs +34 -0
- package/src/components/pagination/pagination.component.ts +85 -0
- package/src/components/query-suggestions/query-suggestions.component.hbs +3 -0
- package/src/components/query-suggestions/query-suggestions.component.ts +66 -0
- package/src/components/range-slider/range-slider.component.hbs +9 -0
- package/src/components/range-slider/range-slider.component.ts +190 -0
- package/src/components/search/search.component.hbs +4 -0
- package/src/components/search/search.component.ts +93 -0
- package/src/components/search-results/search-results.component.hbs +29 -0
- package/src/components/search-results/search-results.component.ts +17 -0
- package/src/components/search-results-item/search-results-item.component.hbs +23 -0
- package/src/components/search-results-item/search-results-item.component.ts +99 -0
- package/src/components/search-results-list/search-results-list.component.hbs +15 -0
- package/src/components/search-results-list/search-results-list.component.ts +24 -0
- package/src/components/selected-facets/selected-facets.component.hbs +11 -0
- package/src/components/selected-facets/selected-facets.component.ts +81 -0
- package/src/components/sorting/sorting.component.hbs +7 -0
- package/src/components/sorting/sorting.component.ts +33 -0
- package/src/components/tabs/tabs.component.hbs +7 -0
- package/src/components/tabs/tabs.component.ts +35 -0
- package/src/components/tooltip/tooltip.component.hbs +6 -0
- package/src/components/tooltip/tooltip.component.ts +48 -0
- package/src/helpers/formatters.ts +15 -0
- package/src/helpers/index.ts +1 -0
- package/src/index.ts +125 -0
- package/src/models/autocomplete.models.ts +71 -0
- package/src/models/component.models.ts +27 -0
- package/src/models/global.models.ts +78 -0
- package/src/models/index.ts +7 -0
- package/src/models/raw-autocomplete.models.ts +46 -0
- package/src/models/raw-search.models.ts +130 -0
- package/src/models/search.models.ts +144 -0
- package/src/models/tracking.models.ts +33 -0
- package/src/scss/components/_autocomplete.scss +55 -0
- package/src/scss/components/_facets.scss +287 -0
- package/src/scss/components/_icons.scss +17 -0
- package/src/scss/components/_modified-query.scss +11 -0
- package/src/scss/components/_page-size.scss +2 -0
- package/src/scss/components/_pagination.scss +23 -0
- package/src/scss/components/_query-suggestions.scss +7 -0
- package/src/scss/components/_range-slider.scss +50 -0
- package/src/scss/components/_search-results-item.scss +64 -0
- package/src/scss/components/_search.scss +4 -0
- package/src/scss/components/_selected-facets.scss +24 -0
- package/src/scss/components/_sorting.scss +2 -0
- package/src/scss/components/_tabs.scss +51 -0
- package/src/scss/components/_tooltips.scss +29 -0
- package/src/scss/general/_forms.scss +82 -0
- package/src/scss/general/_grid-layout.scss +44 -0
- package/src/scss/general/_images.scss +3 -0
- package/src/scss/general/_typography.scss +15 -0
- package/src/scss/hawksearch.scss +35 -0
- package/src/scss/includes/_breakpoints.scss +51 -0
- package/src/scss/includes/_components.scss +14 -0
- package/src/scss/includes/_general.scss +4 -0
- package/src/scss/includes/_reset.scss +203 -0
- package/src/scss/includes/_utility-classes.scss +85 -0
- package/src/services/autocomplete.service.ts +104 -0
- package/src/services/base.service.ts +93 -0
- package/src/services/index.ts +3 -0
- package/src/services/search.service.ts +754 -0
- package/src/services/tracking.service.ts +136 -0
- package/tsconfig.json +22 -0
- package/typings.d.ts +21 -0
- 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,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,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>
|