@searchspring/snap-controller 0.65.2 → 0.66.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 (25) hide show
  1. package/dist/cjs/Autocomplete/AutocompleteController.d.ts +2 -1
  2. package/dist/cjs/Autocomplete/AutocompleteController.d.ts.map +1 -1
  3. package/dist/cjs/Autocomplete/AutocompleteController.js +35 -26
  4. package/dist/cjs/Recommendation/RecommendationController.d.ts +2 -3
  5. package/dist/cjs/Recommendation/RecommendationController.d.ts.map +1 -1
  6. package/dist/cjs/Recommendation/RecommendationController.js +39 -31
  7. package/dist/cjs/Search/SearchController.d.ts +1 -1
  8. package/dist/cjs/Search/SearchController.d.ts.map +1 -1
  9. package/dist/cjs/Search/SearchController.js +110 -68
  10. package/dist/cjs/utils/isClickWithinProductLink.d.ts +5 -0
  11. package/dist/cjs/utils/isClickWithinProductLink.d.ts.map +1 -0
  12. package/dist/cjs/utils/isClickWithinProductLink.js +22 -0
  13. package/dist/esm/Autocomplete/AutocompleteController.d.ts +2 -1
  14. package/dist/esm/Autocomplete/AutocompleteController.d.ts.map +1 -1
  15. package/dist/esm/Autocomplete/AutocompleteController.js +30 -22
  16. package/dist/esm/Recommendation/RecommendationController.d.ts +2 -3
  17. package/dist/esm/Recommendation/RecommendationController.d.ts.map +1 -1
  18. package/dist/esm/Recommendation/RecommendationController.js +30 -26
  19. package/dist/esm/Search/SearchController.d.ts +1 -1
  20. package/dist/esm/Search/SearchController.d.ts.map +1 -1
  21. package/dist/esm/Search/SearchController.js +94 -54
  22. package/dist/esm/utils/isClickWithinProductLink.d.ts +5 -0
  23. package/dist/esm/utils/isClickWithinProductLink.d.ts.map +1 -0
  24. package/dist/esm/utils/isClickWithinProductLink.js +17 -0
  25. package/package.json +10 -10
@@ -2,6 +2,7 @@ import deepmerge from 'deepmerge';
2
2
  import { ErrorType } from '@searchspring/snap-store-mobx';
3
3
  import { AbstractController } from '../Abstract/AbstractController';
4
4
  import { ControllerTypes } from '../types';
5
+ import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
5
6
  const defaultConfig = {
6
7
  id: 'recommend',
7
8
  tag: '',
@@ -25,17 +26,21 @@ export class RecommendationController extends AbstractController {
25
26
  this.tracker.events.recommendations.clickThrough({ data, siteId: this.config.globals?.siteId });
26
27
  this.events.product[result.id] = this.events.product[result.id] || {};
27
28
  this.events.product[result.id].clickThrough = true;
28
- this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, products: [result], trackEvent: data });
29
+ this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
29
30
  },
30
31
  click: (e, result) => {
31
- // TODO: closest might be going too far - write own function to only go n levels up - additionally check that href includes result.url
32
- const href = e.target?.getAttribute('href') || e.target?.closest('a')?.getAttribute('href');
33
- if (href) {
34
- this.track.product.clickThrough(e, result);
32
+ if (this.events.product[result.id]?.click) {
33
+ return;
35
34
  }
36
- else {
37
- // TODO: in future, send as an interaction event
35
+ if (result.type === 'banner') {
36
+ return;
38
37
  }
38
+ isClickWithinProductLink(e, result) && this.track.product.clickThrough(e, result);
39
+ this.events.product[result.id] = this.events.product[result.id] || {};
40
+ this.events.product[result.id].click = true;
41
+ setTimeout(() => {
42
+ this.events.product[result.id].click = false;
43
+ }, CLICK_DUPLICATION_TIMEOUT);
39
44
  },
40
45
  impression: (result) => {
41
46
  if (this.events.product[result.id]?.impression)
@@ -44,7 +49,7 @@ export class RecommendationController extends AbstractController {
44
49
  this.tracker.events.recommendations.impression({ data, siteId: this.config.globals?.siteId });
45
50
  this.events.product[result.id] = this.events.product[result.id] || {};
46
51
  this.events.product[result.id].impression = true;
47
- this.eventManager.fire('track.product.impression', { controller: this, products: [result], trackEvent: data });
52
+ this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
48
53
  return data;
49
54
  },
50
55
  render: (result) => {
@@ -54,23 +59,13 @@ export class RecommendationController extends AbstractController {
54
59
  this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
55
60
  this.events.product[result.id] = this.events.product[result.id] || {};
56
61
  this.events.product[result.id].render = true;
57
- this.eventManager.fire('track.product.render', { controller: this, products: [result], trackEvent: data });
62
+ this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
58
63
  return data;
59
64
  },
60
65
  addToCart: (result) => {
61
66
  const data = getRecommendationsAddtocartSchemaData({ store: this.store, results: [result] });
62
67
  this.tracker.events.recommendations.addToCart({ data, siteId: this.config.globals?.siteId });
63
- this.eventManager.fire('track.product.addToCart', { controller: this, products: [result], trackEvent: data });
64
- return data;
65
- },
66
- },
67
- bundle: {
68
- addToCart: (results) => {
69
- if (this.store.profile.type != 'bundle')
70
- return;
71
- const data = getRecommendationsAddtocartSchemaData({ store: this.store, results });
72
- this.tracker.events.recommendations.addToCart({ data, siteId: this.config.globals?.siteId });
73
- this.eventManager.fire('track.bundle.addToCart', { controller: this, products: results, trackEvent: data });
68
+ this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
74
69
  return data;
75
70
  },
76
71
  },
@@ -194,6 +189,15 @@ export class RecommendationController extends AbstractController {
194
189
  this.store.loading = false;
195
190
  }
196
191
  };
192
+ this.addToCart = async (_products) => {
193
+ const products = typeof _products.slice == 'function' ? _products.slice() : [_products];
194
+ products.forEach((product) => {
195
+ this.track.product.addToCart(product);
196
+ });
197
+ if (products.length > 0) {
198
+ this.eventManager.fire('addToCart', { controller: this, products });
199
+ }
200
+ };
197
201
  if (!config.tag) {
198
202
  throw new Error(`Invalid config passed to RecommendationController. The "tag" attribute is required.`);
199
203
  }
@@ -220,8 +224,8 @@ export class RecommendationController extends AbstractController {
220
224
  products.forEach((result) => {
221
225
  this.events.product[result.id] = this.events.product[result.id] || {};
222
226
  this.events.product[result.id].render = true;
227
+ this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
223
228
  });
224
- this.eventManager.fire('track.product.render', { controller: this, products, trackEvent: data });
225
229
  }
226
230
  });
227
231
  // add 'afterStore' middleware
@@ -269,9 +273,9 @@ function getRecommendationsAddtocartSchemaData({ store, results, }) {
269
273
  results: results?.map((result) => {
270
274
  const core = result.mappings.core;
271
275
  return {
272
- uid: core.uid || '',
273
- sku: core.sku,
274
- price: Number(core.price),
276
+ uid: core?.uid || '',
277
+ sku: core?.sku,
278
+ price: Number(core?.price),
275
279
  qty: result.quantity || 1,
276
280
  };
277
281
  }) || [],
@@ -282,11 +286,11 @@ function getRecommendationsSchemaData({ store, results }) {
282
286
  tag: store.profile.tag,
283
287
  results: results?.map((result) => {
284
288
  const core = result.mappings.core;
289
+ const position = result.position;
285
290
  return {
291
+ position,
286
292
  uid: core.uid || '',
287
- // childUid: core.uid,
288
293
  sku: core.sku,
289
- // childSku: core.sku,
290
294
  };
291
295
  }) || [],
292
296
  };
@@ -26,7 +26,7 @@ export declare class SearchController extends AbstractController {
26
26
  track: SearchTrackMethods;
27
27
  get params(): SearchRequestModel;
28
28
  search: () => Promise<void>;
29
- addToCart: (product: Product) => Promise<void>;
29
+ addToCart: (_products: Product[] | Product) => Promise<void>;
30
30
  }
31
31
  export declare function getStorableRequestParams(request: SearchRequestModel): SearchRequestModel;
32
32
  export declare function generateHrefSelector(element: HTMLElement, href: string, levels?: number): string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"SearchController.d.ts","sourceRoot":"","sources":["../../../src/Search/SearchController.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAa,MAAM,+BAA+B,CAAC;AAExE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAClF,OAAO,KAAK,EACX,sBAAsB,EAGtB,kBAAkB,EAClB,gBAAgB,EAIhB,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EACX,kBAAkB,EAOlB,MAAM,2BAA2B,CAAC;AAgCnC,KAAK,kBAAkB,GAAG;IACzB,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACvD,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAClC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACtC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;IACvC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,eAAe,CAAwC;IAC/D,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,MAAM,CASM;gBAGnB,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAqN3B,KAAK,EAAE,kBAAkB,CA0FvB;IAEF,IAAI,MAAM,IAAI,kBAAkB,CA8C/B;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CAuN9B;IAEF,SAAS,YAAmB,OAAO,KAAG,QAAQ,IAAI,CAAC,CAGjD;CACF;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAiBxF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,MAAM,GAAG,SAAS,CAgCvG"}
1
+ {"version":3,"file":"SearchController.d.ts","sourceRoot":"","sources":["../../../src/Search/SearchController.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAa,MAAM,+BAA+B,CAAC;AAExE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAClF,OAAO,KAAK,EACX,sBAAsB,EAGtB,kBAAkB,EAClB,gBAAgB,EAIhB,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EACX,kBAAkB,EAQlB,MAAM,2BAA2B,CAAC;AAkCnC,KAAK,kBAAkB,GAAG;IACzB,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACvD,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAClC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACtC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AAGF,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;IACvC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,eAAe,CAAwC;IAC/D,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,MAAM,CAUM;gBAGnB,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAuN3B,KAAK,EAAE,kBAAkB,CA+FvB;IAEF,IAAI,MAAM,IAAI,kBAAkB,CA8C/B;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CAyN9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAQ/D;CACF;AAwBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAiBxF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,MAAM,GAAG,SAAS,CAgCvG"}
@@ -4,6 +4,7 @@ import { AbstractController } from '../Abstract/AbstractController';
4
4
  import { StorageStore, ErrorType } from '@searchspring/snap-store-mobx';
5
5
  import { getSearchParams } from '../utils/getParams';
6
6
  import { ControllerTypes } from '../types';
7
+ import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
7
8
  const BACKGROUND_FILTER_FIELD_MATCHES = ['collection', 'category', 'categories', 'hierarchy'];
8
9
  const BACKGROUND_FILTERS_VALUE_FLAGS = [1, 0, '1', '0', 'true', 'false', true, false];
9
10
  const defaultConfig = {
@@ -22,6 +23,7 @@ const defaultConfig = {
22
23
  },
23
24
  },
24
25
  };
26
+ const schemaMap = {};
25
27
  export class SearchController extends AbstractController {
26
28
  constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context) {
27
29
  super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
@@ -61,52 +63,53 @@ export class SearchController extends AbstractController {
61
63
  }
62
64
  // store position data or empty object
63
65
  this.storage.set('scrollMap', scrollMap);
64
- const data = getSearchSchemaData({ params: this.params, store: this.store, results: [result] });
66
+ const data = schemaMap[result.id];
65
67
  this.tracker.events[this.pageType].clickThrough({ data, siteId: this.config.globals?.siteId });
66
68
  this.events.product[result.id] = this.events.product[result.id] || {};
67
69
  this.events.product[result.id].clickThrough = true;
68
- this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, products: [result], trackEvent: data });
70
+ this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
69
71
  },
70
72
  click: (e, result) => {
71
- if (result.type === 'banner') {
73
+ if (this.events.product[result.id]?.click) {
72
74
  return;
73
75
  }
74
- // TODO: closest might be going too far - write own function to only go n levels up - additionally check that href includes result.url
75
- const href = e.target?.getAttribute('href') || e.target?.closest('a')?.getAttribute('href');
76
- if (href) {
77
- this.track.product.clickThrough(e, result);
78
- }
79
- else {
80
- // TODO: in future, send as an interaction event
76
+ if (result.type === 'banner') {
77
+ return;
81
78
  }
79
+ isClickWithinProductLink(e, result) && this.track.product.clickThrough(e, result);
80
+ this.events.product[result.id] = this.events.product[result.id] || {};
81
+ this.events.product[result.id].click = true;
82
+ setTimeout(() => {
83
+ this.events.product[result.id].click = false;
84
+ }, CLICK_DUPLICATION_TIMEOUT);
82
85
  },
83
86
  render: (result) => {
84
87
  if (this.events.product[result.id]?.render) {
85
88
  return;
86
89
  }
87
- const data = getSearchSchemaData({ params: this.params, store: this.store, results: result ? [result] : [] });
90
+ const data = schemaMap[result.id];
88
91
  this.tracker.events[this.pageType].render({ data, siteId: this.config.globals?.siteId });
89
92
  this.events.product[result.id] = this.events.product[result.id] || {};
90
93
  this.events.product[result.id].render = true;
91
- this.eventManager.fire('track.product.render', { controller: this, products: [result], trackEvent: data });
94
+ this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
92
95
  },
93
96
  impression: (result) => {
94
97
  if (this.events.product[result.id]?.impression) {
95
98
  return;
96
99
  }
97
- const data = getSearchSchemaData({ params: this.params, store: this.store, results: [result] });
100
+ const data = schemaMap[result.id];
98
101
  this.tracker.events[this.pageType].impression({ data, siteId: this.config.globals?.siteId });
99
102
  this.events.product[result.id] = this.events.product[result.id] || {};
100
103
  this.events.product[result.id].impression = true;
101
- this.eventManager.fire('track.product.impression', { controller: this, products: [result], trackEvent: data });
104
+ this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
102
105
  },
103
106
  addToCart: (result) => {
104
- const data = getSearchAddtocartSchemaData({ params: this.params, store: this.store, results: [result] });
107
+ const data = getSearchAddtocartSchemaData({ searchSchemaData: schemaMap[result.id], results: [result] });
105
108
  this.tracker.events[this.pageType].addToCart({
106
109
  data,
107
110
  siteId: this.config.globals?.siteId,
108
111
  });
109
- this.eventManager.fire('track.product.addToCart', { controller: this, products: [result], trackEvent: data });
112
+ this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
110
113
  },
111
114
  },
112
115
  redirect: (redirectURL) => {
@@ -142,7 +145,7 @@ export class SearchController extends AbstractController {
142
145
  throw err;
143
146
  }
144
147
  }
145
- const stringyParams = JSON.stringify(params);
148
+ const stringyParams = JSON.stringify(getStorableRequestParams(params));
146
149
  const prevStringyParams = this.storage.get('lastStringyParams');
147
150
  if (stringyParams == prevStringyParams) {
148
151
  // no param change - not searching
@@ -164,6 +167,7 @@ export class SearchController extends AbstractController {
164
167
  // infinite backfill is enabled AND we have not yet fetched any results
165
168
  if (this.config.settings?.infinite.backfill && !this.store.loaded) {
166
169
  // create requests for all missing pages (using Arrray(page).fill() to populate an array to map)
170
+ const backfillRequestsParams = [];
167
171
  const backfillRequests = Array(params.pagination.page)
168
172
  .fill('backfill')
169
173
  .map((v, i) => {
@@ -177,6 +181,7 @@ export class SearchController extends AbstractController {
177
181
  delete backfillParams?.search?.redirectResponse;
178
182
  }
179
183
  }
184
+ backfillRequestsParams.push(backfillParams);
180
185
  return this.client.search(backfillParams);
181
186
  });
182
187
  const backfillResponses = await Promise.all(backfillRequests);
@@ -185,8 +190,8 @@ export class SearchController extends AbstractController {
185
190
  meta = backfillResponses[0][0];
186
191
  response = backfillResponses[0][1];
187
192
  // accumulate results from all backfill responses
188
- const backfillResults = backfillResponses.reduce((results, response) => {
189
- // response is [meta, searchResponse]
193
+ const backfillResults = backfillResponses.reduce((results, response, index) => {
194
+ createResultSchemaMapping({ request: backfillRequestsParams[index], response: response });
190
195
  return results.concat(...response[1].results);
191
196
  }, []);
192
197
  // overwrite pagination params to expected state
@@ -198,6 +203,7 @@ export class SearchController extends AbstractController {
198
203
  else {
199
204
  // infinite with no backfills.
200
205
  [meta, response] = await this.client.search(params);
206
+ createResultSchemaMapping({ request: params, response: [meta, response] });
201
207
  // append new results to previous results
202
208
  response.results = [...this.previousResults, ...(response.results || [])];
203
209
  }
@@ -209,6 +215,7 @@ export class SearchController extends AbstractController {
209
215
  // clear previousResults to prevent infinite scroll from using them
210
216
  this.previousResults = [];
211
217
  [meta, response] = await this.client.search(params);
218
+ createResultSchemaMapping({ request: params, response: [meta, response] });
212
219
  }
213
220
  // MockClient will overwrite the client search() method and use SearchData to return mock data which already contains meta data
214
221
  if (!response.meta) {
@@ -308,9 +315,14 @@ export class SearchController extends AbstractController {
308
315
  this.store.loading = false;
309
316
  }
310
317
  };
311
- this.addToCart = async (product) => {
312
- this.track.product.addToCart(product);
313
- this.eventManager.fire('addToCart', { controller: this, products: [product] });
318
+ this.addToCart = async (_products) => {
319
+ const products = typeof _products.slice == 'function' ? _products.slice() : [_products];
320
+ products.forEach((product) => {
321
+ this.track.product.addToCart(product);
322
+ });
323
+ if (products.length > 0) {
324
+ this.eventManager.fire('addToCart', { controller: this, products });
325
+ }
314
326
  };
315
327
  // deep merge config with defaults
316
328
  this.config = deepmerge(defaultConfig, this.config);
@@ -372,25 +384,14 @@ export class SearchController extends AbstractController {
372
384
  window.location.replace(redirectURL);
373
385
  return false;
374
386
  }
375
- const nonBackgroundFilters = search?.request?.filters?.filter((filter) => !filter.background);
376
- if (config?.settings?.redirects?.singleResult &&
377
- search?.response?.search?.query &&
378
- search?.response?.pagination?.totalResults === 1 &&
379
- !nonBackgroundFilters?.length) {
380
- //set loaded to true to prevent infinite search/reloading from happening
381
- searchStore.loaded = true;
382
- window.location.replace(search?.response.results[0].mappings.core.url);
383
- return false;
384
- }
385
387
  await next();
386
388
  });
387
389
  this.eventManager.on('afterStore', async (search, next) => {
388
390
  await next();
389
- // save last params
390
- this.storage.set('lastStringyParams', JSON.stringify(search.request));
391
391
  // get scrollTo positioning and send it to 'restorePosition' event
392
392
  const storableRequestParams = getStorableRequestParams(search.request);
393
393
  const stringyParams = JSON.stringify(storableRequestParams);
394
+ this.storage.set('lastStringyParams', stringyParams);
394
395
  const scrollMap = this.storage.get('scrollMap') || {};
395
396
  const elementPosition = scrollMap[stringyParams];
396
397
  if (!elementPosition) {
@@ -405,14 +406,27 @@ export class SearchController extends AbstractController {
405
406
  const controller = search.controller;
406
407
  if (controller.store.loaded && !controller.store.error) {
407
408
  const products = controller.store.results.filter((result) => result.type === 'product' && !this.events.product[result.id]?.render);
408
- const results = products.length === 0 ? [] : products;
409
- const data = getSearchSchemaData({ params: this.params, store: this.store, results });
410
- this.tracker.events[this.pageType].render({ data, siteId: this.config.globals?.siteId });
409
+ if (products.length === 0) {
410
+ // handle no results
411
+ const data = getSearchSchemaData({ params: search.request, results: [] });
412
+ this.tracker.events[this.pageType].render({ data, siteId: this.config.globals?.siteId });
413
+ }
411
414
  products.forEach((result) => {
415
+ const data = schemaMap[result.id];
416
+ this.tracker.events[this.pageType].render({ data, siteId: this.config.globals?.siteId });
412
417
  this.events.product[result.id] = this.events.product[result.id] || {};
413
418
  this.events.product[result.id].render = true;
419
+ this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
414
420
  });
415
- this.eventManager.fire('track.product.render', { controller: this, products, trackEvent: data });
421
+ const config = search.controller.config;
422
+ const nonBackgroundFilters = search?.request?.filters?.filter((filter) => !filter.background);
423
+ if (config?.settings?.redirects?.singleResult &&
424
+ search?.response?.search?.query &&
425
+ search?.response?.pagination?.totalResults === 1 &&
426
+ !nonBackgroundFilters?.length) {
427
+ window.location.replace(search?.response.results[0].mappings.core.url);
428
+ return false;
429
+ }
416
430
  }
417
431
  });
418
432
  // restore position
@@ -528,6 +542,26 @@ export class SearchController extends AbstractController {
528
542
  return params;
529
543
  }
530
544
  }
545
+ function createResultSchemaMapping({ request, response }) {
546
+ const [_, searchResponse] = response;
547
+ const schema = getSearchSchemaData({
548
+ params: request,
549
+ results: [], // results added below because this would contain all results
550
+ response: searchResponse,
551
+ });
552
+ searchResponse.results?.forEach((result) => {
553
+ schemaMap[result.id] = {
554
+ ...schema,
555
+ results: [
556
+ {
557
+ position: result.position,
558
+ uid: result.mappings?.core?.uid || '',
559
+ sku: result.mappings?.core?.sku,
560
+ },
561
+ ],
562
+ };
563
+ });
564
+ }
531
565
  export function getStorableRequestParams(request) {
532
566
  return {
533
567
  siteId: request.siteId,
@@ -575,10 +609,9 @@ function getSearchRedirectSchemaData({ redirectURL }) {
575
609
  redirect: redirectURL,
576
610
  };
577
611
  }
578
- function getSearchAddtocartSchemaData({ params, store, results, }) {
579
- const base = getSearchSchemaData({ params, store, results });
612
+ function getSearchAddtocartSchemaData({ searchSchemaData, results, }) {
580
613
  return {
581
- ...base,
614
+ ...searchSchemaData,
582
615
  results: results?.map((result) => {
583
616
  const core = result.mappings.core;
584
617
  return {
@@ -590,7 +623,7 @@ function getSearchAddtocartSchemaData({ params, store, results, }) {
590
623
  }) || [],
591
624
  };
592
625
  }
593
- function getSearchSchemaData({ params, store, results }) {
626
+ function getSearchSchemaData({ params, results, response, }) {
594
627
  const filters = params.filters?.reduce((acc, filter) => {
595
628
  const key = filter.background ? 'bgfilter' : 'filter';
596
629
  acc[key] = acc[key] || [];
@@ -611,9 +644,16 @@ function getSearchSchemaData({ params, store, results }) {
611
644
  }
612
645
  return acc;
613
646
  }, {});
647
+ let correctedQuery;
648
+ if (response?.search?.originalQuery && response?.search?.query) {
649
+ correctedQuery = response?.search?.query;
650
+ }
651
+ const campaigns = response?.merchandising?.campaigns || [];
652
+ const experiments = response?.merchandising?.experiments || [];
614
653
  return {
615
654
  q: params.search?.query?.string || '',
616
- correctedQuery: store.search?.originalQuery?.string ? store.search?.query?.string : undefined,
655
+ correctedQuery,
656
+ matchType: response?.search?.matchType,
617
657
  ...filters,
618
658
  sort: params.sorts?.map((sort) => {
619
659
  return {
@@ -622,16 +662,16 @@ function getSearchSchemaData({ params, store, results }) {
622
662
  };
623
663
  }),
624
664
  pagination: {
625
- totalResults: store.pagination.totalResults,
626
- page: store.pagination.page,
627
- resultsPerPage: store.pagination.pageSize,
665
+ totalResults: response?.pagination?.totalResults,
666
+ page: response?.pagination?.page,
667
+ resultsPerPage: response?.pagination?.pageSize,
628
668
  },
629
669
  merchandising: {
630
- personalized: store.merchandising.personalized,
631
- redirect: store.merchandising.redirect,
632
- triggeredCampaigns: (store.merchandising.campaigns?.length &&
633
- store.merchandising.campaigns?.map((campaign) => {
634
- const experiement = store.merchandising.experiments.find((experiment) => experiment.campaignId === campaign.id);
670
+ personalized: response?.merchandising?.personalized,
671
+ redirect: response?.merchandising?.redirect,
672
+ triggeredCampaigns: (campaigns.length &&
673
+ campaigns.map((campaign) => {
674
+ const experiement = experiments.find((experiment) => experiment.campaignId === campaign.id);
635
675
  return {
636
676
  id: campaign.id,
637
677
  experimentId: experiement?.experimentId,
@@ -641,12 +681,12 @@ function getSearchSchemaData({ params, store, results }) {
641
681
  undefined,
642
682
  },
643
683
  results: results?.map((result) => {
644
- const core = result.mappings.core;
684
+ const core = result.mappings?.core;
685
+ const position = result.position;
645
686
  return {
687
+ position,
646
688
  uid: core.uid || '',
647
- // childUid: core.uid,
648
689
  sku: core.sku,
649
- // childSku: core.sku,
650
690
  };
651
691
  }) || [],
652
692
  };
@@ -0,0 +1,5 @@
1
+ import type { Product } from '@searchspring/snap-store-mobx';
2
+ export declare const CLICK_DUPLICATION_TIMEOUT = 300;
3
+ export declare const CLICK_THROUGH_CLOSEST_MAX_LEVELS = 12;
4
+ export declare const isClickWithinProductLink: (e: MouseEvent, result: Product) => boolean;
5
+ //# sourceMappingURL=isClickWithinProductLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isClickWithinProductLink.d.ts","sourceRoot":"","sources":["../../../src/utils/isClickWithinProductLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAE7D,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD,eAAO,MAAM,wBAAwB,MAAO,UAAU,UAAU,OAAO,KAAG,OAezE,CAAC"}
@@ -0,0 +1,17 @@
1
+ export const CLICK_DUPLICATION_TIMEOUT = 300;
2
+ export const CLICK_THROUGH_CLOSEST_MAX_LEVELS = 12;
3
+ export const isClickWithinProductLink = (e, result) => {
4
+ let currentElement = e.target;
5
+ let href = null;
6
+ let level = 0;
7
+ const resultUrl = result?.display?.mappings.core?.url || result?.mappings.core?.url || '';
8
+ while (currentElement && level < CLICK_THROUGH_CLOSEST_MAX_LEVELS) {
9
+ href = currentElement.getAttribute('href');
10
+ if (href && resultUrl && href.includes(resultUrl)) {
11
+ return true;
12
+ }
13
+ currentElement = currentElement.parentElement;
14
+ level++;
15
+ }
16
+ return false;
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searchspring/snap-controller",
3
- "version": "0.65.2",
3
+ "version": "0.66.0",
4
4
  "description": "Snap Controllers",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -20,22 +20,22 @@
20
20
  "test:watch": "jest --watch"
21
21
  },
22
22
  "dependencies": {
23
- "@searchspring/snap-toolbox": "^0.65.2",
23
+ "@searchspring/snap-toolbox": "^0.66.0",
24
24
  "css.escape": "1.5.1",
25
25
  "deepmerge": "4.3.1"
26
26
  },
27
27
  "devDependencies": {
28
- "@searchspring/snap-client": "^0.65.2",
29
- "@searchspring/snap-event-manager": "^0.65.2",
30
- "@searchspring/snap-logger": "^0.65.2",
31
- "@searchspring/snap-profiler": "^0.65.2",
32
- "@searchspring/snap-store-mobx": "^0.65.2",
33
- "@searchspring/snap-tracker": "^0.65.2",
34
- "@searchspring/snap-url-manager": "^0.65.2"
28
+ "@searchspring/snap-client": "^0.66.0",
29
+ "@searchspring/snap-event-manager": "^0.66.0",
30
+ "@searchspring/snap-logger": "^0.66.0",
31
+ "@searchspring/snap-profiler": "^0.66.0",
32
+ "@searchspring/snap-store-mobx": "^0.66.0",
33
+ "@searchspring/snap-tracker": "^0.66.0",
34
+ "@searchspring/snap-url-manager": "^0.66.0"
35
35
  },
36
36
  "sideEffects": false,
37
37
  "files": [
38
38
  "dist/**/*"
39
39
  ],
40
- "gitHead": "3e80b62664af7e3104e578d6c2596c439d5dfd78"
40
+ "gitHead": "97d209ef85cfb63946bea8ce3737b7dcb2803b89"
41
41
  }