@athoscommerce/snap-store-mobx 1.0.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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/cjs/Abstract/AbstractStore.d.ts +17 -0
  4. package/dist/cjs/Abstract/AbstractStore.d.ts.map +1 -0
  5. package/dist/cjs/Abstract/AbstractStore.js +28 -0
  6. package/dist/cjs/Autocomplete/AutocompleteStore.d.ts +40 -0
  7. package/dist/cjs/Autocomplete/AutocompleteStore.d.ts.map +1 -0
  8. package/dist/cjs/Autocomplete/AutocompleteStore.js +246 -0
  9. package/dist/cjs/Autocomplete/Stores/AutocompleteFacetStore.d.ts +13 -0
  10. package/dist/cjs/Autocomplete/Stores/AutocompleteFacetStore.d.ts.map +1 -0
  11. package/dist/cjs/Autocomplete/Stores/AutocompleteFacetStore.js +67 -0
  12. package/dist/cjs/Autocomplete/Stores/AutocompleteHistoryStore.d.ts +19 -0
  13. package/dist/cjs/Autocomplete/Stores/AutocompleteHistoryStore.d.ts.map +1 -0
  14. package/dist/cjs/Autocomplete/Stores/AutocompleteHistoryStore.js +57 -0
  15. package/dist/cjs/Autocomplete/Stores/AutocompleteQueryStore.d.ts +25 -0
  16. package/dist/cjs/Autocomplete/Stores/AutocompleteQueryStore.d.ts.map +1 -0
  17. package/dist/cjs/Autocomplete/Stores/AutocompleteQueryStore.js +34 -0
  18. package/dist/cjs/Autocomplete/Stores/AutocompleteStateStore.d.ts +29 -0
  19. package/dist/cjs/Autocomplete/Stores/AutocompleteStateStore.d.ts.map +1 -0
  20. package/dist/cjs/Autocomplete/Stores/AutocompleteStateStore.js +54 -0
  21. package/dist/cjs/Autocomplete/Stores/AutocompleteTermStore.d.ts +47 -0
  22. package/dist/cjs/Autocomplete/Stores/AutocompleteTermStore.d.ts.map +1 -0
  23. package/dist/cjs/Autocomplete/Stores/AutocompleteTermStore.js +102 -0
  24. package/dist/cjs/Autocomplete/Stores/AutocompleteTrendingStore.d.ts +20 -0
  25. package/dist/cjs/Autocomplete/Stores/AutocompleteTrendingStore.d.ts.map +1 -0
  26. package/dist/cjs/Autocomplete/Stores/AutocompleteTrendingStore.js +58 -0
  27. package/dist/cjs/Autocomplete/Stores/index.d.ts +7 -0
  28. package/dist/cjs/Autocomplete/Stores/index.d.ts.map +1 -0
  29. package/dist/cjs/Autocomplete/Stores/index.js +16 -0
  30. package/dist/cjs/Cart/CartStore.d.ts +15 -0
  31. package/dist/cjs/Cart/CartStore.d.ts.map +1 -0
  32. package/dist/cjs/Cart/CartStore.js +109 -0
  33. package/dist/cjs/Finder/FinderStore.d.ts +27 -0
  34. package/dist/cjs/Finder/FinderStore.d.ts.map +1 -0
  35. package/dist/cjs/Finder/FinderStore.js +156 -0
  36. package/dist/cjs/Finder/Stores/FinderSelectionStore.d.ts +73 -0
  37. package/dist/cjs/Finder/Stores/FinderSelectionStore.d.ts.map +1 -0
  38. package/dist/cjs/Finder/Stores/FinderSelectionStore.js +320 -0
  39. package/dist/cjs/Finder/Stores/index.d.ts +2 -0
  40. package/dist/cjs/Finder/Stores/index.d.ts.map +1 -0
  41. package/dist/cjs/Finder/Stores/index.js +5 -0
  42. package/dist/cjs/Meta/MetaStore.d.ts +20 -0
  43. package/dist/cjs/Meta/MetaStore.d.ts.map +1 -0
  44. package/dist/cjs/Meta/MetaStore.js +55 -0
  45. package/dist/cjs/Recommendation/RecommendationStore.d.ts +19 -0
  46. package/dist/cjs/Recommendation/RecommendationStore.d.ts.map +1 -0
  47. package/dist/cjs/Recommendation/RecommendationStore.js +97 -0
  48. package/dist/cjs/Recommendation/Stores/RecommendationProfileStore.d.ts +15 -0
  49. package/dist/cjs/Recommendation/Stores/RecommendationProfileStore.d.ts.map +1 -0
  50. package/dist/cjs/Recommendation/Stores/RecommendationProfileStore.js +30 -0
  51. package/dist/cjs/Recommendation/Stores/index.d.ts +2 -0
  52. package/dist/cjs/Recommendation/Stores/index.d.ts.map +1 -0
  53. package/dist/cjs/Recommendation/Stores/index.js +5 -0
  54. package/dist/cjs/Search/SearchStore.d.ts +27 -0
  55. package/dist/cjs/Search/SearchStore.d.ts.map +1 -0
  56. package/dist/cjs/Search/SearchStore.js +125 -0
  57. package/dist/cjs/Search/Stores/SearchFacetStore.d.ts +89 -0
  58. package/dist/cjs/Search/Stores/SearchFacetStore.d.ts.map +1 -0
  59. package/dist/cjs/Search/Stores/SearchFacetStore.js +368 -0
  60. package/dist/cjs/Search/Stores/SearchFilterStore.d.ts +44 -0
  61. package/dist/cjs/Search/Stores/SearchFilterStore.d.ts.map +1 -0
  62. package/dist/cjs/Search/Stores/SearchFilterStore.js +102 -0
  63. package/dist/cjs/Search/Stores/SearchHistoryStore.d.ts +20 -0
  64. package/dist/cjs/Search/Stores/SearchHistoryStore.d.ts.map +1 -0
  65. package/dist/cjs/Search/Stores/SearchHistoryStore.js +106 -0
  66. package/dist/cjs/Search/Stores/SearchMerchandisingStore.d.ts +36 -0
  67. package/dist/cjs/Search/Stores/SearchMerchandisingStore.d.ts.map +1 -0
  68. package/dist/cjs/Search/Stores/SearchMerchandisingStore.js +90 -0
  69. package/dist/cjs/Search/Stores/SearchPaginationStore.d.ts +57 -0
  70. package/dist/cjs/Search/Stores/SearchPaginationStore.d.ts.map +1 -0
  71. package/dist/cjs/Search/Stores/SearchPaginationStore.js +212 -0
  72. package/dist/cjs/Search/Stores/SearchQueryStore.d.ts +23 -0
  73. package/dist/cjs/Search/Stores/SearchQueryStore.d.ts.map +1 -0
  74. package/dist/cjs/Search/Stores/SearchQueryStore.js +41 -0
  75. package/dist/cjs/Search/Stores/SearchResultStore.d.ts +175 -0
  76. package/dist/cjs/Search/Stores/SearchResultStore.d.ts.map +1 -0
  77. package/dist/cjs/Search/Stores/SearchResultStore.js +749 -0
  78. package/dist/cjs/Search/Stores/SearchSortingStore.d.ts +32 -0
  79. package/dist/cjs/Search/Stores/SearchSortingStore.d.ts.map +1 -0
  80. package/dist/cjs/Search/Stores/SearchSortingStore.js +77 -0
  81. package/dist/cjs/Search/Stores/index.d.ts +9 -0
  82. package/dist/cjs/Search/Stores/index.d.ts.map +1 -0
  83. package/dist/cjs/Search/Stores/index.js +30 -0
  84. package/dist/cjs/Storage/StorageStore.d.ts +27 -0
  85. package/dist/cjs/Storage/StorageStore.d.ts.map +1 -0
  86. package/dist/cjs/Storage/StorageStore.js +176 -0
  87. package/dist/cjs/index.d.ts +12 -0
  88. package/dist/cjs/index.d.ts.map +1 -0
  89. package/dist/cjs/index.js +37 -0
  90. package/dist/cjs/types.d.ts +170 -0
  91. package/dist/cjs/types.d.ts.map +1 -0
  92. package/dist/cjs/types.js +9 -0
  93. package/dist/esm/Abstract/AbstractStore.d.ts +17 -0
  94. package/dist/esm/Abstract/AbstractStore.d.ts.map +1 -0
  95. package/dist/esm/Abstract/AbstractStore.js +22 -0
  96. package/dist/esm/Autocomplete/AutocompleteStore.d.ts +40 -0
  97. package/dist/esm/Autocomplete/AutocompleteStore.d.ts.map +1 -0
  98. package/dist/esm/Autocomplete/AutocompleteStore.js +210 -0
  99. package/dist/esm/Autocomplete/Stores/AutocompleteFacetStore.d.ts +13 -0
  100. package/dist/esm/Autocomplete/Stores/AutocompleteFacetStore.d.ts.map +1 -0
  101. package/dist/esm/Autocomplete/Stores/AutocompleteFacetStore.js +35 -0
  102. package/dist/esm/Autocomplete/Stores/AutocompleteHistoryStore.d.ts +19 -0
  103. package/dist/esm/Autocomplete/Stores/AutocompleteHistoryStore.d.ts.map +1 -0
  104. package/dist/esm/Autocomplete/Stores/AutocompleteHistoryStore.js +27 -0
  105. package/dist/esm/Autocomplete/Stores/AutocompleteQueryStore.d.ts +25 -0
  106. package/dist/esm/Autocomplete/Stores/AutocompleteQueryStore.d.ts.map +1 -0
  107. package/dist/esm/Autocomplete/Stores/AutocompleteQueryStore.js +28 -0
  108. package/dist/esm/Autocomplete/Stores/AutocompleteStateStore.d.ts +29 -0
  109. package/dist/esm/Autocomplete/Stores/AutocompleteStateStore.d.ts.map +1 -0
  110. package/dist/esm/Autocomplete/Stores/AutocompleteStateStore.js +42 -0
  111. package/dist/esm/Autocomplete/Stores/AutocompleteTermStore.d.ts +47 -0
  112. package/dist/esm/Autocomplete/Stores/AutocompleteTermStore.d.ts.map +1 -0
  113. package/dist/esm/Autocomplete/Stores/AutocompleteTermStore.js +56 -0
  114. package/dist/esm/Autocomplete/Stores/AutocompleteTrendingStore.d.ts +20 -0
  115. package/dist/esm/Autocomplete/Stores/AutocompleteTrendingStore.d.ts.map +1 -0
  116. package/dist/esm/Autocomplete/Stores/AutocompleteTrendingStore.js +27 -0
  117. package/dist/esm/Autocomplete/Stores/index.d.ts +7 -0
  118. package/dist/esm/Autocomplete/Stores/index.d.ts.map +1 -0
  119. package/dist/esm/Autocomplete/Stores/index.js +6 -0
  120. package/dist/esm/Cart/CartStore.d.ts +15 -0
  121. package/dist/esm/Cart/CartStore.d.ts.map +1 -0
  122. package/dist/esm/Cart/CartStore.js +74 -0
  123. package/dist/esm/Finder/FinderStore.d.ts +27 -0
  124. package/dist/esm/Finder/FinderStore.d.ts.map +1 -0
  125. package/dist/esm/Finder/FinderStore.js +118 -0
  126. package/dist/esm/Finder/Stores/FinderSelectionStore.d.ts +73 -0
  127. package/dist/esm/Finder/Stores/FinderSelectionStore.d.ts.map +1 -0
  128. package/dist/esm/Finder/Stores/FinderSelectionStore.js +258 -0
  129. package/dist/esm/Finder/Stores/index.d.ts +2 -0
  130. package/dist/esm/Finder/Stores/index.d.ts.map +1 -0
  131. package/dist/esm/Finder/Stores/index.js +1 -0
  132. package/dist/esm/Meta/MetaStore.d.ts +20 -0
  133. package/dist/esm/Meta/MetaStore.d.ts.map +1 -0
  134. package/dist/esm/Meta/MetaStore.js +45 -0
  135. package/dist/esm/Recommendation/RecommendationStore.d.ts +19 -0
  136. package/dist/esm/Recommendation/RecommendationStore.d.ts.map +1 -0
  137. package/dist/esm/Recommendation/RecommendationStore.js +71 -0
  138. package/dist/esm/Recommendation/Stores/RecommendationProfileStore.d.ts +15 -0
  139. package/dist/esm/Recommendation/Stores/RecommendationProfileStore.d.ts.map +1 -0
  140. package/dist/esm/Recommendation/Stores/RecommendationProfileStore.js +25 -0
  141. package/dist/esm/Recommendation/Stores/index.d.ts +2 -0
  142. package/dist/esm/Recommendation/Stores/index.d.ts.map +1 -0
  143. package/dist/esm/Recommendation/Stores/index.js +1 -0
  144. package/dist/esm/Search/SearchStore.d.ts +27 -0
  145. package/dist/esm/Search/SearchStore.d.ts.map +1 -0
  146. package/dist/esm/Search/SearchStore.js +102 -0
  147. package/dist/esm/Search/Stores/SearchFacetStore.d.ts +89 -0
  148. package/dist/esm/Search/Stores/SearchFacetStore.d.ts.map +1 -0
  149. package/dist/esm/Search/Stores/SearchFacetStore.js +292 -0
  150. package/dist/esm/Search/Stores/SearchFilterStore.d.ts +44 -0
  151. package/dist/esm/Search/Stores/SearchFilterStore.d.ts.map +1 -0
  152. package/dist/esm/Search/Stores/SearchFilterStore.js +70 -0
  153. package/dist/esm/Search/Stores/SearchHistoryStore.d.ts +20 -0
  154. package/dist/esm/Search/Stores/SearchHistoryStore.d.ts.map +1 -0
  155. package/dist/esm/Search/Stores/SearchHistoryStore.js +85 -0
  156. package/dist/esm/Search/Stores/SearchMerchandisingStore.d.ts +36 -0
  157. package/dist/esm/Search/Stores/SearchMerchandisingStore.d.ts.map +1 -0
  158. package/dist/esm/Search/Stores/SearchMerchandisingStore.js +61 -0
  159. package/dist/esm/Search/Stores/SearchPaginationStore.d.ts +57 -0
  160. package/dist/esm/Search/Stores/SearchPaginationStore.d.ts.map +1 -0
  161. package/dist/esm/Search/Stores/SearchPaginationStore.js +163 -0
  162. package/dist/esm/Search/Stores/SearchQueryStore.d.ts +23 -0
  163. package/dist/esm/Search/Stores/SearchQueryStore.d.ts.map +1 -0
  164. package/dist/esm/Search/Stores/SearchQueryStore.js +33 -0
  165. package/dist/esm/Search/Stores/SearchResultStore.d.ts +175 -0
  166. package/dist/esm/Search/Stores/SearchResultStore.d.ts.map +1 -0
  167. package/dist/esm/Search/Stores/SearchResultStore.js +648 -0
  168. package/dist/esm/Search/Stores/SearchSortingStore.d.ts +32 -0
  169. package/dist/esm/Search/Stores/SearchSortingStore.d.ts.map +1 -0
  170. package/dist/esm/Search/Stores/SearchSortingStore.js +67 -0
  171. package/dist/esm/Search/Stores/index.d.ts +9 -0
  172. package/dist/esm/Search/Stores/index.d.ts.map +1 -0
  173. package/dist/esm/Search/Stores/index.js +8 -0
  174. package/dist/esm/Storage/StorageStore.d.ts +27 -0
  175. package/dist/esm/Storage/StorageStore.d.ts.map +1 -0
  176. package/dist/esm/Storage/StorageStore.js +169 -0
  177. package/dist/esm/index.d.ts +12 -0
  178. package/dist/esm/index.d.ts.map +1 -0
  179. package/dist/esm/index.js +11 -0
  180. package/dist/esm/types.d.ts +170 -0
  181. package/dist/esm/types.d.ts.map +1 -0
  182. package/dist/esm/types.js +6 -0
  183. package/package.json +35 -0
@@ -0,0 +1,648 @@
1
+ import { computed, makeObservable, observable } from 'mobx';
2
+ import deepmerge from 'deepmerge';
3
+ import { isPlainObject } from 'is-plain-object';
4
+ const VARIANT_ATTRIBUTE = 'ss-variant-option';
5
+ const VARIANT_ATTRIBUTE_SELECTED = 'ss-variant-option-selected';
6
+ export class SearchResultStore extends Array {
7
+ static get [Symbol.species]() {
8
+ return Array;
9
+ }
10
+ constructor(params) {
11
+ const { config, data, state, stores } = params || {};
12
+ const { search, meta, previousSearch } = data || {};
13
+ const { results, merchandising, pagination } = search || {};
14
+ const { previousResults } = stores || {};
15
+ const { loaded } = state || {};
16
+ let resultsArr = (results || []).map((result, idx) => {
17
+ return new Product({
18
+ config,
19
+ data: { result, meta },
20
+ position: idx + 1,
21
+ responseId: params.data.search?.tracking?.responseId || '',
22
+ });
23
+ });
24
+ const variantConfig = config?.settings?.variants;
25
+ // preselected variant options
26
+ if (variantConfig?.realtime?.enabled) {
27
+ // attach click events - ONLY happens once (known limitation)
28
+ if (!loaded && resultsArr?.length) {
29
+ const processedSelects = new Set();
30
+ document.querySelectorAll(`[${VARIANT_ATTRIBUTE}]`).forEach((elem) => {
31
+ if (elem.tagName == 'OPTION') {
32
+ const parentSelect = elem.closest('select');
33
+ if (parentSelect) {
34
+ if (!processedSelects.has(parentSelect)) {
35
+ processedSelects.add(parentSelect);
36
+ parentSelect.addEventListener('change', (e) => {
37
+ const val = e.target?.value;
38
+ const clickedOption = Array.from(parentSelect.querySelectorAll(`[${VARIANT_ATTRIBUTE}]`)).filter((elem) => elem.value == val);
39
+ if (clickedOption.length > 0) {
40
+ variantOptionClick(clickedOption[0], variantConfig, resultsArr);
41
+ }
42
+ });
43
+ }
44
+ }
45
+ else {
46
+ console.warn('Warning: unable to add realtime variant event listener for element - ', elem);
47
+ }
48
+ }
49
+ else {
50
+ elem.addEventListener('click', () => {
51
+ variantOptionClick(elem, variantConfig, resultsArr);
52
+ });
53
+ }
54
+ });
55
+ }
56
+ // check for attributes for preselection
57
+ if (resultsArr.length) {
58
+ const options = {};
59
+ // grab values from elements on the page to form preselected elements
60
+ document.querySelectorAll(`[${VARIANT_ATTRIBUTE_SELECTED}]`).forEach((elem) => {
61
+ const attr = elem.getAttribute(VARIANT_ATTRIBUTE);
62
+ if (attr) {
63
+ const [option, value] = attr.split(':');
64
+ if (option && value) {
65
+ options[option.toLowerCase()] = [value.toLowerCase()];
66
+ }
67
+ }
68
+ });
69
+ makeVariantSelections(variantConfig, options, resultsArr);
70
+ }
71
+ }
72
+ // only when infinite is enabled
73
+ if (config?.settings?.infinite?.enabled && previousResults) {
74
+ // logic to determine when to concatenate previous results
75
+ // this logic is not bullet proof, but it is highly unlikely that the current and previous pagination data would ever be sequential unless paginating
76
+ // TODO: implement "load previous" with a limit to how many total products can be displayed
77
+ // the limit would be enforced by unloading (trimming) results based on the direction of the new search (previous page, next page)
78
+ if (pagination?.page && previousSearch?.pagination?.page && pagination.page == previousSearch.pagination.page + 1) {
79
+ resultsArr = (previousResults || []).concat(resultsArr);
80
+ }
81
+ }
82
+ // add banners to results - banner data includes ALL banners within the entire search results (even if not on the current page)
83
+ if (pagination && merchandising?.content?.inline) {
84
+ // ensure banners are sorted by index
85
+ const banners = merchandising.content.inline
86
+ .sort(function (a, b) {
87
+ return a.config.position.index - b.config.position.index;
88
+ })
89
+ .map((banner) => {
90
+ return new Banner({
91
+ data: { banner, responseId: params.data.search?.tracking?.responseId || '' },
92
+ });
93
+ });
94
+ if (banners && pagination.totalResults) {
95
+ resultsArr = addBannersToResults(config, resultsArr, banners, pagination);
96
+ }
97
+ }
98
+ super(...resultsArr);
99
+ }
100
+ }
101
+ export class Banner {
102
+ constructor(bannerData) {
103
+ this.type = 'banner';
104
+ this.attributes = {};
105
+ this.mappings = {
106
+ core: {},
107
+ };
108
+ this.custom = {};
109
+ const { banner, responseId } = bannerData?.data || {};
110
+ const htmlString = banner.value;
111
+ const match = typeof htmlString === 'string' && htmlString.match(/data-banner-id="(\d+)"/);
112
+ const uid = match ? match[1] : 'ss-ib-' + banner.config.position.index;
113
+ this.id = uid;
114
+ this.responseId = responseId;
115
+ this.config = banner.config;
116
+ this.value = banner.value;
117
+ makeObservable(this, {
118
+ id: observable,
119
+ mappings: observable,
120
+ attributes: observable,
121
+ });
122
+ }
123
+ }
124
+ export class Product {
125
+ constructor(productData) {
126
+ this.type = 'product';
127
+ this.attributes = {};
128
+ this.mappings = {
129
+ core: {},
130
+ };
131
+ this.custom = {};
132
+ this.quantity = 1;
133
+ this.mask = new ProductMask();
134
+ const { config } = productData || {};
135
+ const { result, meta } = productData?.data || {};
136
+ this.id = result.id;
137
+ this.attributes = result.attributes;
138
+ this.mappings = result.mappings;
139
+ this.position = productData.position;
140
+ this.badges = new Badges({
141
+ data: {
142
+ meta,
143
+ result,
144
+ },
145
+ });
146
+ this.responseId = result.responseId || productData.responseId;
147
+ // @ts-ignore - need to add bundleSeed to snapi-types
148
+ if (result.bundleSeed) {
149
+ // @ts-ignore - need to add bundleSeed to snapi-types
150
+ this.bundleSeed = Boolean(result.bundleSeed);
151
+ }
152
+ if (result.variants && result.variants.data) {
153
+ // if variants are already parsed, use them
154
+ this.variants = new Variants({
155
+ data: {
156
+ mask: this.mask,
157
+ variants: result.variants.data,
158
+ optionConfig: result.variants.optionConfig,
159
+ preferences: result.variants?.preferences, // TODO: fix typing
160
+ meta: meta,
161
+ },
162
+ config: config?.settings?.variants,
163
+ });
164
+ }
165
+ makeObservable(this, {
166
+ id: observable,
167
+ display: computed,
168
+ mappings: observable,
169
+ attributes: observable,
170
+ custom: observable,
171
+ quantity: observable,
172
+ });
173
+ }
174
+ get display() {
175
+ return deepmerge({ id: this.id, mappings: this.mappings, attributes: this.attributes, badges: this.badges }, this.mask.data, {
176
+ isMergeableObject: isPlainObject,
177
+ });
178
+ }
179
+ }
180
+ export class Badges {
181
+ constructor(badgesData) {
182
+ this.all = [];
183
+ const { data } = badgesData || {};
184
+ const { meta, result } = data || {};
185
+ this.all = (result.badges || [])
186
+ .filter((badge) => {
187
+ // remove badges that are not in the meta or are disabled
188
+ return !!(badge?.tag && meta.badges?.tags && meta.badges?.tags[badge.tag] && meta.badges?.tags[badge.tag].enabled);
189
+ })
190
+ .map((badge) => {
191
+ // merge badge with badge meta data
192
+ const metaBadgeData = meta.badges?.tags?.[badge.tag];
193
+ return {
194
+ ...badge,
195
+ ...metaBadgeData,
196
+ };
197
+ })
198
+ .sort((a, b) => {
199
+ return a.priority - b.priority;
200
+ });
201
+ makeObservable(this, {
202
+ all: observable,
203
+ tags: computed,
204
+ locations: computed,
205
+ });
206
+ }
207
+ // get all the result badges that are in a specific location
208
+ atLocation(location) {
209
+ const locations = Array.isArray(location) ? location : [location];
210
+ return this.all.filter((badge) => {
211
+ // filter location
212
+ return locations.some((location) => badge.location.startsWith(`${location}/`) || badge.location == location);
213
+ });
214
+ }
215
+ get tags() {
216
+ return this.all.reduce((badgeMap, badge) => {
217
+ badgeMap[badge.tag] = badge;
218
+ return badgeMap;
219
+ }, {});
220
+ }
221
+ get locations() {
222
+ return this.all.reduce((locationMap, badge) => {
223
+ // put badge in location by path
224
+ const [section, tag] = badge.location.split('/');
225
+ locationMap[section] = locationMap[section] || {};
226
+ locationMap[section][tag] = (locationMap[section][tag] || []).concat(badge);
227
+ return locationMap;
228
+ }, {});
229
+ }
230
+ }
231
+ // Mask is used to power the product display for quick attribute swapping
232
+ export class ProductMask {
233
+ constructor() {
234
+ this.data = {};
235
+ makeObservable(this, {
236
+ data: observable,
237
+ });
238
+ }
239
+ merge(mask) {
240
+ // TODO: look into making more performant
241
+ // needed to prevent infinite re-render on merge with same data
242
+ if (JSON.stringify(deepmerge(this.data, mask)) != JSON.stringify(this.data)) {
243
+ this.data = deepmerge(this.data, mask);
244
+ }
245
+ }
246
+ set(mask) {
247
+ // TODO: look into making more performant
248
+ // needed to prevent infinite re-render on set with same data
249
+ if (JSON.stringify(mask) != JSON.stringify(this.data)) {
250
+ this.data = mask;
251
+ }
252
+ }
253
+ reset() {
254
+ this.data = {};
255
+ }
256
+ }
257
+ export class Variants {
258
+ constructor(variantData) {
259
+ this.data = [];
260
+ this.selections = [];
261
+ const { config, data } = variantData || {};
262
+ const { variants, mask, meta } = data || {};
263
+ const preferences = variantData?.data?.preferences || {};
264
+ // setting function in constructor to prevent exposing mask as class property
265
+ this.setActive = (variant) => {
266
+ this.active = variant;
267
+ const activeBadges = new Badges({
268
+ data: {
269
+ meta: meta,
270
+ result: variant,
271
+ },
272
+ });
273
+ mask.set({ mappings: this.active.mappings, attributes: this.active.attributes, badges: activeBadges });
274
+ };
275
+ if (config) {
276
+ this.config = config;
277
+ }
278
+ if (data.optionConfig) {
279
+ this.optionConfig = data.optionConfig;
280
+ }
281
+ this.update(variants, config, preferences);
282
+ }
283
+ update(variantData, config = this.config, preferences) {
284
+ try {
285
+ const options = [];
286
+ // create variants objects
287
+ this.data = variantData
288
+ .filter((variant) => this.config?.showDisabledSelectionValues || variant.mappings.core?.available !== false)
289
+ .map((variant) => {
290
+ // normalize price fields ensuring they are numbers
291
+ if (variant.mappings.core?.price) {
292
+ variant.mappings.core.price = Number(variant.mappings.core?.price);
293
+ }
294
+ if (variant.mappings.core?.msrp) {
295
+ variant.mappings.core.msrp = Number(variant.mappings.core?.msrp);
296
+ }
297
+ return variant;
298
+ })
299
+ .map((variant) => {
300
+ Object.keys(variant.options).forEach((variantOption) => {
301
+ if (!options.includes(variantOption)) {
302
+ options.push(variantOption);
303
+ }
304
+ });
305
+ return new Variant({
306
+ data: { variant },
307
+ });
308
+ });
309
+ //need to reset this.selections first
310
+ this.selections = [];
311
+ options.map((option) => {
312
+ const variantOptionConfig = this.config?.options && this.config.options[option];
313
+ this.selections.push(new VariantSelection({
314
+ config: variantOptionConfig,
315
+ optionConfig: this.optionConfig?.[option],
316
+ data: {
317
+ variants: this,
318
+ selectorField: option,
319
+ },
320
+ }));
321
+ });
322
+ const preselectedOptions = {};
323
+ //api preferences
324
+ if (preferences) {
325
+ Object.keys(preferences).forEach((option) => {
326
+ preselectedOptions[option] = preferences[option];
327
+ });
328
+ }
329
+ //user defined preferences - these override API preferences
330
+ if (config?.options) {
331
+ Object.keys(config?.options).forEach((option) => {
332
+ if (config.options[option].preSelected) {
333
+ preselectedOptions[option] = config.options[option].preSelected;
334
+ }
335
+ });
336
+ }
337
+ if (config?.autoSelect) {
338
+ // select first available
339
+ this.makeSelections(preselectedOptions);
340
+ }
341
+ }
342
+ catch (err) {
343
+ // failed to parse the variant JSON
344
+ console.error(err, `Invalid variant JSON for: ${variantData}`);
345
+ }
346
+ }
347
+ makeSelections(options) {
348
+ // options = {color: 'Blue', size: 'L'};
349
+ if (!options || !Object.keys(options).length) {
350
+ // select first available for each selection
351
+ this.selections.forEach((selection) => {
352
+ const firstAvailableOption = selection.values.find((value) => value.available);
353
+ if (firstAvailableOption) {
354
+ selection.select(firstAvailableOption.value, true);
355
+ }
356
+ });
357
+ }
358
+ else {
359
+ this.selections.forEach((selection, idx) => {
360
+ // filter by first available, then by preselected option preference
361
+ //make all options available for first selection.
362
+ const availableOptions = selection.values.filter((value) => (idx == 0 ? true : value.available));
363
+ const preferedOptions = options[selection.field.toLowerCase()];
364
+ let preferencedOption = selection.selected || availableOptions[0];
365
+ // if theres a preference for that field
366
+ if (preferedOptions) {
367
+ const checkIfAvailable = (preference) => {
368
+ //see if that option is in the available options
369
+ const availablePreferedOptions = availableOptions.find((value) => value.value.toString().toLowerCase() == preference?.toString().toLowerCase());
370
+ //use it
371
+ if (availablePreferedOptions) {
372
+ preferencedOption = availablePreferedOptions;
373
+ }
374
+ };
375
+ if (Array.isArray(preferedOptions)) {
376
+ //loop through each preference option
377
+ preferedOptions.forEach((preference) => {
378
+ checkIfAvailable(preference);
379
+ });
380
+ }
381
+ else {
382
+ checkIfAvailable(preferedOptions);
383
+ }
384
+ }
385
+ if (preferencedOption) {
386
+ selection.select(preferencedOption.value, true);
387
+ }
388
+ });
389
+ }
390
+ }
391
+ refineSelections(fromSelection) {
392
+ // need to ensure the update originator is at the BOTTOM of the list for refinement
393
+ const orderedSelections = [...this.selections];
394
+ orderedSelections.sort((a) => {
395
+ if (a.field == fromSelection.field) {
396
+ return 1;
397
+ }
398
+ return -1;
399
+ });
400
+ // refine selections ensuring that the selection that triggered the update refines LAST
401
+ orderedSelections.forEach((selection) => selection.refineValues(this));
402
+ // check to see if we have enough selections made to update the display
403
+ const selectedSelections = this.selections.filter((selection) => selection.selected?.value?.length);
404
+ if (selectedSelections.length) {
405
+ let availableVariants = this.data;
406
+ // loop through selectedSelections and only include available products that match current selections
407
+ for (const selectedSelection of selectedSelections) {
408
+ availableVariants = availableVariants.filter((variant) => selectedSelection.selected?.value == variant.options[selectedSelection.field]?.value && variant.available);
409
+ }
410
+ // set active variant
411
+ if (availableVariants.length == 1) {
412
+ const variantToSet = availableVariants[0];
413
+ //need to update all unselected selections to match the single result
414
+ const unselectedSelections = this.selections.filter((selection) => !selection.selected);
415
+ unselectedSelections.forEach((selection) => {
416
+ const field = selection.field;
417
+ const valueToSet = variantToSet.options[field].value;
418
+ selection.select(valueToSet);
419
+ });
420
+ this.setActive(variantToSet);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ export class VariantSelection {
426
+ constructor(variantSelectionData) {
427
+ this.selected = undefined;
428
+ this.previouslySelected = undefined;
429
+ this.values = [];
430
+ const { data, config, optionConfig } = variantSelectionData || {};
431
+ const { variants, selectorField } = data || {};
432
+ this.field = selectorField;
433
+ this.type = optionConfig?.type;
434
+ this.count = optionConfig?.count;
435
+ this.label = config?.label || selectorField;
436
+ this.config = config || {};
437
+ // needed to prevent attaching variants as class property
438
+ this.variantsUpdate = () => variants.refineSelections(this);
439
+ // create possible values from the data and refine them
440
+ this.refineValues(variants);
441
+ makeObservable(this, {
442
+ selected: observable,
443
+ values: observable,
444
+ });
445
+ }
446
+ refineValues(variants) {
447
+ // current selection should only consider OTHER selections for availability
448
+ const selectedSelections = variants.selections.filter((selection) => selection.field != this.field && selection.selected);
449
+ let availableVariants = variants.data.filter((variant) => variant.available);
450
+ // loop through selectedSelections and remove products that do not match
451
+ for (const selectedSelection of selectedSelections) {
452
+ availableVariants = availableVariants.filter((variant) => selectedSelection.selected?.value == variant.options?.[selectedSelection.field]?.value && variant.available);
453
+ }
454
+ const newValues = variants.data
455
+ .filter((variant) => variant.options[this.field])
456
+ .reduce((values, variant) => {
457
+ if (!values.some((val) => variant.options[this.field].value == val.value)) {
458
+ const value = variant.options[this.field].value;
459
+ const thumbnailImageUrl = variant.mappings.core?.thumbnailImageUrl;
460
+ // A value should only be disabled if there are NO available variants in the entire dataset that have this value for the current field
461
+ const allAvailableVariants = variants.data.filter((variant) => variant.available);
462
+ const disabledValue = !allAvailableVariants.some((availableVariant) => {
463
+ return availableVariant.options[this.field].value === value;
464
+ });
465
+ const mappedValue = {
466
+ value: value,
467
+ label: value,
468
+ thumbnailImageUrl: thumbnailImageUrl,
469
+ available: Boolean(availableVariants.some((availableVariant) => availableVariant.options[this.field].value == variant.options[this.field].value)),
470
+ disabled: disabledValue,
471
+ };
472
+ if (this.config.thumbnailBackgroundImages) {
473
+ mappedValue.backgroundImageUrl = thumbnailImageUrl;
474
+ }
475
+ else if (variant.options[this.field].backgroundImageUrl) {
476
+ mappedValue.backgroundImageUrl = variant.options[this.field].backgroundImageUrl;
477
+ }
478
+ if (variant.options[this.field].background) {
479
+ mappedValue.background = variant.options[this.field].background;
480
+ }
481
+ if (this.config.mappings && this.config.mappings && this.config.mappings[value.toString().toLowerCase()]) {
482
+ const mapping = this.config.mappings[value.toString().toLowerCase()];
483
+ if (mapping.label) {
484
+ mappedValue.label = mapping.label;
485
+ }
486
+ if (mapping.background) {
487
+ mappedValue.background = mapping.background;
488
+ }
489
+ if (mapping.backgroundImageUrl) {
490
+ mappedValue.backgroundImageUrl = mapping.backgroundImageUrl;
491
+ }
492
+ }
493
+ values.push(mappedValue);
494
+ }
495
+ // TODO: use sorting function from config
496
+ return values;
497
+ }, []);
498
+ // if selection has been made
499
+ if (this.selected) {
500
+ // check if the selection is stil available
501
+ if (!newValues.some((val) => val.value == this.selected?.value && val.available)) {
502
+ // the selection is no longer available, attempt to select previous selection
503
+ if (this.selected !== this.previouslySelected &&
504
+ this.previouslySelected &&
505
+ newValues.some((val) => val.value == this.previouslySelected?.value && val.available)) {
506
+ this.select(this.previouslySelected.value, true);
507
+ }
508
+ else {
509
+ // choose the first available option if previous seletions are unavailable
510
+ const availableValues = newValues.filter((val) => val.available);
511
+ if (newValues.length && availableValues.length) {
512
+ const nextAvailableValue = availableValues[0].value;
513
+ if (this.selected.value !== nextAvailableValue) {
514
+ this.select(nextAvailableValue, true);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+ this.values = newValues;
521
+ }
522
+ reset() {
523
+ this.selected = undefined;
524
+ this.values.forEach((val) => (val.available = false));
525
+ }
526
+ select(value, internalSelection = false) {
527
+ const valueExist = this.values.find((val) => val.value == value);
528
+ if (valueExist) {
529
+ if (!internalSelection) {
530
+ this.previouslySelected = this.selected;
531
+ }
532
+ this.selected = valueExist;
533
+ this.variantsUpdate();
534
+ }
535
+ }
536
+ }
537
+ export class Variant {
538
+ constructor(variantData) {
539
+ this.type = 'variant';
540
+ this.attributes = {};
541
+ this.mappings = {
542
+ core: {},
543
+ };
544
+ this.custom = {};
545
+ const { data } = variantData || {};
546
+ const { variant } = data || {};
547
+ this.attributes = variant.attributes || {};
548
+ this.mappings = variant.mappings;
549
+ this.options = variant.options;
550
+ // construct badges from data (need meta)
551
+ this.badges = variant.badges || [];
552
+ this.available = this.mappings.core?.available ?? true;
553
+ makeObservable(this, {
554
+ attributes: observable,
555
+ mappings: observable,
556
+ custom: observable,
557
+ available: observable,
558
+ });
559
+ }
560
+ }
561
+ function addBannersToResults(config, results, allBanners, paginationData) {
562
+ const bannersAndResults = [...results];
563
+ let paginationBegin = paginationData.pageSize * (paginationData.page - 1) + 1;
564
+ let paginationEnd = paginationData.pageSize * paginationData.page;
565
+ // if infinite scroll is enabled, we need to adjust the begin position
566
+ if (config?.settings?.infinite?.enabled)
567
+ paginationBegin = 1;
568
+ // if the end of the page is greater than the total results, adjust the end
569
+ if (paginationData.pageSize * paginationData.page > paginationData.totalResults)
570
+ paginationEnd = paginationData.totalResults;
571
+ // banners that have not allready been injected
572
+ const bannersNotInResults = allBanners.filter((banner) => !bannersAndResults.some((result) => result.id == banner.id));
573
+ // banners that have an index position within the current set of results
574
+ const bannersToInject = bannersNotInResults.filter((banner) => {
575
+ // find banners that are within the current pagination set
576
+ const index = banner.config.position.index;
577
+ return index >= paginationBegin - 1 && index <= paginationEnd - 1;
578
+ });
579
+ // banners can have an index greater than the total results, these should be injected at the end
580
+ const bannersToInjectAtEnd = bannersNotInResults.filter((banner) => {
581
+ const index = banner.config.position.index;
582
+ return index >= paginationData.totalResults;
583
+ });
584
+ // inject banners that have index position within current set into the results
585
+ bannersToInject.forEach((banner) => {
586
+ const adjustedIndex = banner.config.position.index - (paginationBegin - 1);
587
+ bannersAndResults.splice(adjustedIndex, 0, banner);
588
+ });
589
+ // inject banners at the end that fall within the current pagination set (but ensure there is room based on the begin and end indexes)
590
+ bannersToInjectAtEnd.forEach((banner, index) => {
591
+ const resultIndex = paginationData.totalResults - (bannersToInjectAtEnd.length - index);
592
+ if (resultIndex >= paginationBegin - 1 && resultIndex <= paginationEnd - 1) {
593
+ bannersAndResults.splice(resultIndex, 0, banner);
594
+ }
595
+ });
596
+ // when using infinite, need to adjust the inline banner responseIds
597
+ if (config?.settings?.infinite) {
598
+ bannersAndResults.forEach((item, index) => {
599
+ if (item.type === 'banner') {
600
+ // need to determine what the correctResponseId is based on the index position, pageSize (current page) and adjacent product responseIds
601
+ const pageSize = paginationData.pageSize;
602
+ const currentPage = Math.floor(index / pageSize) + 1;
603
+ const firstItemIndexOnPage = (currentPage - 1) * pageSize;
604
+ const lastItemIndexOnPage = firstItemIndexOnPage + pageSize - 1;
605
+ // find the first product on the current page to grab its responseId
606
+ for (let i = firstItemIndexOnPage; i < lastItemIndexOnPage; i++) {
607
+ if (bannersAndResults[i].type === 'product') {
608
+ item.responseId = bannersAndResults[i].responseId;
609
+ break;
610
+ }
611
+ }
612
+ }
613
+ });
614
+ }
615
+ return bannersAndResults;
616
+ }
617
+ function variantOptionClick(elem, variantConfig, results) {
618
+ const options = {};
619
+ const attr = elem.getAttribute(VARIANT_ATTRIBUTE);
620
+ if (attr) {
621
+ const [option, value] = attr.split(':');
622
+ if (!option || !value) {
623
+ console.error('Error!: realtime variant is missing option or value (option:value)!', elem, attr);
624
+ }
625
+ else {
626
+ options[option.toLowerCase()] = [value.toLowerCase()];
627
+ makeVariantSelections(variantConfig, options, results);
628
+ }
629
+ }
630
+ }
631
+ function makeVariantSelections(variantConfig, options, results) {
632
+ let filteredResults = results;
633
+ // filter based on config
634
+ variantConfig.realtime?.filters?.forEach((filter) => {
635
+ if (filter == 'first') {
636
+ filteredResults = [filteredResults[0]];
637
+ }
638
+ if (filter == 'unaltered') {
639
+ filteredResults = filteredResults.filter((result) => !result.variants?.selections.some((selection) => selection.previouslySelected));
640
+ }
641
+ });
642
+ filteredResults.forEach((result) => {
643
+ // no banner types
644
+ if (result.type == 'product') {
645
+ result.variants?.makeSelections(options);
646
+ }
647
+ });
648
+ }
@@ -0,0 +1,32 @@
1
+ import type { UrlManager } from '@athoscommerce/snap-url-manager';
2
+ import type { StoreServices } from '../../types';
3
+ import type { MetaResponseModel, MetaResponseModelSortOption, SearchResponseModel } from '@athoscommerce/snapi-types';
4
+ type MetaResponseModelSortOptionMutated = MetaResponseModelSortOption & {
5
+ active?: boolean;
6
+ default?: boolean;
7
+ };
8
+ type SearchSortingStoreConfig = {
9
+ services: StoreServices;
10
+ data: {
11
+ search?: SearchResponseModel;
12
+ meta: MetaResponseModel;
13
+ };
14
+ };
15
+ export declare class SearchSortingStore {
16
+ options: Option[];
17
+ constructor(params: SearchSortingStoreConfig);
18
+ get current(): Option | undefined;
19
+ }
20
+ declare class Option {
21
+ active: boolean;
22
+ default: boolean;
23
+ field: string;
24
+ label: string;
25
+ direction: string;
26
+ type: string;
27
+ value: string;
28
+ url: UrlManager;
29
+ constructor(services: StoreServices, option: MetaResponseModelSortOptionMutated, index: number);
30
+ }
31
+ export {};
32
+ //# sourceMappingURL=SearchSortingStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchSortingStore.d.ts","sourceRoot":"","sources":["../../../../src/Search/Stores/SearchSortingStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtH,KAAK,kCAAkC,GAAG,2BAA2B,GAAG;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC/B,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE;QACL,MAAM,CAAC,EAAE,mBAAmB,CAAC;QAC7B,IAAI,EAAE,iBAAiB,CAAC;KACxB,CAAC;CACF,CAAC;AAEF,qBAAa,kBAAkB;IACvB,OAAO,EAAE,MAAM,EAAE,CAAM;gBAElB,MAAM,EAAE,wBAAwB;IA0C5C,IAAW,OAAO,IAAI,MAAM,GAAG,SAAS,CAEvC;CACD;AAED,cAAM,MAAM;IACJ,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,UAAU,CAAC;gBAEX,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,kCAAkC,EAAE,KAAK,EAAE,MAAM;CAuB9F"}