@searchspring/snap-controller 0.65.1 → 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 +79 -32
  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 +73 -25
  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 +141 -61
  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 +66 -28
  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 +56 -23
  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 +123 -53
  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
@@ -12,9 +12,6 @@ type RecommendationTrackMethods = {
12
12
  impression: (result: Product) => void;
13
13
  addToCart: (result: Product) => void;
14
14
  };
15
- bundle: {
16
- addToCart: (results: Product[]) => void;
17
- };
18
15
  };
19
16
  export declare class RecommendationController extends AbstractController {
20
17
  type: ControllerTypes;
@@ -22,6 +19,7 @@ export declare class RecommendationController extends AbstractController {
22
19
  config: RecommendationControllerConfig;
23
20
  events: {
24
21
  product: Record<string, {
22
+ click?: boolean;
25
23
  clickThrough?: boolean;
26
24
  impression?: boolean;
27
25
  render?: boolean;
@@ -31,6 +29,7 @@ export declare class RecommendationController extends AbstractController {
31
29
  track: RecommendationTrackMethods;
32
30
  get params(): RecommendRequestModel;
33
31
  search: () => Promise<void>;
32
+ addToCart: (_products: Product[] | Product) => Promise<void>;
34
33
  }
35
34
  export {};
36
35
  //# sourceMappingURL=RecommendationController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RecommendationController.d.ts","sourceRoot":"","sources":["../../../src/Recommendation/RecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,KAAK,EAAE,8BAA8B,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAIrG,KAAK,0BAA0B,GAAG;IACjC,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,KAAK,IAAI,CAAC;QAChD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAClC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACtC,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;KACrC,CAAC;IACF,MAAM,EAAE;QACP,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;KACxC,CAAC;CACF,CAAC;AAUF,qBAAa,wBAAyB,SAAQ,kBAAkB;IACxD,IAAI,kBAAkC;IACrC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE,8BAA8B,CAAC;IAE/C,MAAM,EAAE;QACP,OAAO,EAAE,MAAM,CACd,MAAM,EACN;YACC,YAAY,CAAC,EAAE,OAAO,CAAC;YACvB,UAAU,CAAC,EAAE,OAAO,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,CAAC;SACjB,CACD,CAAC;KACF,CAEC;gBAGD,MAAM,EAAE,8BAA8B,EACtC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAwC3B,KAAK,EAAE,0BAA0B,CAyD/B;IAEF,IAAI,MAAM,IAAI,qBAAqB,CA4BlC;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CA6H9B;CACF"}
1
+ {"version":3,"file":"RecommendationController.d.ts","sourceRoot":"","sources":["../../../src/Recommendation/RecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,KAAK,EAAE,8BAA8B,EAAE,kBAAkB,EAAE,gBAAgB,EAAiB,MAAM,UAAU,CAAC;AAKpH,KAAK,0BAA0B,GAAG;IACjC,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,KAAK,IAAI,CAAC;QAChD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAClC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACtC,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;KACrC,CAAC;CACF,CAAC;AAUF,qBAAa,wBAAyB,SAAQ,kBAAkB;IACxD,IAAI,kBAAkC;IACrC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE,8BAA8B,CAAC;IAE/C,MAAM,EAAE;QACP,OAAO,EAAE,MAAM,CACd,MAAM,EACN;YACC,KAAK,CAAC,EAAE,OAAO,CAAC;YAChB,YAAY,CAAC,EAAE,OAAO,CAAC;YACvB,UAAU,CAAC,EAAE,OAAO,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,CAAC;SACjB,CACD,CAAC;KACF,CAEC;gBAGD,MAAM,EAAE,8BAA8B,EACtC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAwD3B,KAAK,EAAE,0BAA0B,CAuD/B;IAEF,IAAI,MAAM,IAAI,qBAAqB,CA4BlC;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CA6H9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAQ/D;CACF"}
@@ -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
- const data = getRecommendationsSchemaData({ store: this.store, results: [result] });
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 = getRecommendationsSchemaData({ 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
  }
@@ -209,6 +213,21 @@ export class RecommendationController extends AbstractController {
209
213
  // deep merge config with defaults
210
214
  this.config = deepmerge(defaultConfig, this.config);
211
215
  this.store.setConfig(this.config);
216
+ this.eventManager.on('afterStore', async (search, next) => {
217
+ await next();
218
+ const controller = search.controller;
219
+ if (controller.store.loaded && !controller.store.error) {
220
+ const products = controller.store.results.filter((result) => result.type === 'product');
221
+ const results = products.length === 0 ? [] : products;
222
+ const data = getRecommendationsSchemaData({ store: this.store, results });
223
+ this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
224
+ products.forEach((result) => {
225
+ this.events.product[result.id] = this.events.product[result.id] || {};
226
+ this.events.product[result.id].render = true;
227
+ this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
228
+ });
229
+ }
230
+ });
212
231
  // add 'afterStore' middleware
213
232
  // this.eventManager.on('afterStore', async (recommend: AfterStoreObj, next: Next): Promise<void | boolean> => {
214
233
  // await next();
@@ -248,16 +267,30 @@ export class RecommendationController extends AbstractController {
248
267
  return params;
249
268
  }
250
269
  }
270
+ function getRecommendationsAddtocartSchemaData({ store, results, }) {
271
+ return {
272
+ tag: store.profile.tag,
273
+ results: results?.map((result) => {
274
+ const core = result.mappings.core;
275
+ return {
276
+ uid: core?.uid || '',
277
+ sku: core?.sku,
278
+ price: Number(core?.price),
279
+ qty: result.quantity || 1,
280
+ };
281
+ }) || [],
282
+ };
283
+ }
251
284
  function getRecommendationsSchemaData({ store, results }) {
252
285
  return {
253
286
  tag: store.profile.tag,
254
287
  results: results?.map((result) => {
255
288
  const core = result.mappings.core;
289
+ const position = result.position;
256
290
  return {
291
+ position,
257
292
  uid: core.uid || '',
258
- // childUid: core.uid,
259
293
  sku: core.sku,
260
- // childSku: core.sku,
261
294
  };
262
295
  }) || [],
263
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,EAAqB,MAAM,+BAA+B,CAAC;AACrG,OAAO,KAAK,EACX,sBAAsB,EAGtB,kBAAkB,EAClB,gBAAgB,EAIhB,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EACX,kBAAkB,EAOlB,MAAM,2BAA2B,CAAC;AA8BnC,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;IAmM3B,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] });
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 = getSearchSchemaData({ 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) {
@@ -400,6 +401,34 @@ export class SearchController extends AbstractController {
400
401
  // not awaiting this event as it relies on render, and render is blocked by afterStore event
401
402
  this.eventManager.fire('restorePosition', { controller: this, element: elementPosition });
402
403
  });
404
+ this.eventManager.on('afterStore', async (search, next) => {
405
+ await next();
406
+ const controller = search.controller;
407
+ if (controller.store.loaded && !controller.store.error) {
408
+ const products = controller.store.results.filter((result) => result.type === 'product' && !this.events.product[result.id]?.render);
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
+ }
414
+ products.forEach((result) => {
415
+ const data = schemaMap[result.id];
416
+ this.tracker.events[this.pageType].render({ data, siteId: this.config.globals?.siteId });
417
+ this.events.product[result.id] = this.events.product[result.id] || {};
418
+ this.events.product[result.id].render = true;
419
+ this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
420
+ });
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
+ }
430
+ }
431
+ });
403
432
  // restore position
404
433
  if (this.config.settings?.restorePosition?.enabled) {
405
434
  this.eventManager.on('restorePosition', async ({ controller, element }, next) => {
@@ -513,6 +542,26 @@ export class SearchController extends AbstractController {
513
542
  return params;
514
543
  }
515
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
+ }
516
565
  export function getStorableRequestParams(request) {
517
566
  return {
518
567
  siteId: request.siteId,
@@ -560,7 +609,21 @@ function getSearchRedirectSchemaData({ redirectURL }) {
560
609
  redirect: redirectURL,
561
610
  };
562
611
  }
563
- function getSearchSchemaData({ params, store, results, }) {
612
+ function getSearchAddtocartSchemaData({ searchSchemaData, results, }) {
613
+ return {
614
+ ...searchSchemaData,
615
+ results: results?.map((result) => {
616
+ const core = result.mappings.core;
617
+ return {
618
+ uid: core.uid || '',
619
+ sku: core.sku,
620
+ price: Number(core.price),
621
+ qty: result.quantity || 1,
622
+ };
623
+ }) || [],
624
+ };
625
+ }
626
+ function getSearchSchemaData({ params, results, response, }) {
564
627
  const filters = params.filters?.reduce((acc, filter) => {
565
628
  const key = filter.background ? 'bgfilter' : 'filter';
566
629
  acc[key] = acc[key] || [];
@@ -581,27 +644,34 @@ function getSearchSchemaData({ params, store, results, }) {
581
644
  }
582
645
  return acc;
583
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 || [];
584
653
  return {
585
654
  q: params.search?.query?.string || '',
586
- correctedQuery: store.search?.originalQuery?.string ? store.search?.query?.string : undefined,
655
+ correctedQuery,
656
+ matchType: response?.search?.matchType,
587
657
  ...filters,
588
- sort: [
589
- {
590
- field: store.sorting.current?.field,
591
- dir: store.sorting.current?.direction,
592
- },
593
- ],
658
+ sort: params.sorts?.map((sort) => {
659
+ return {
660
+ field: sort.field,
661
+ dir: sort.direction,
662
+ };
663
+ }),
594
664
  pagination: {
595
- totalResults: store.pagination.totalResults,
596
- page: store.pagination.page,
597
- resultsPerPage: store.pagination.pageSize,
665
+ totalResults: response?.pagination?.totalResults,
666
+ page: response?.pagination?.page,
667
+ resultsPerPage: response?.pagination?.pageSize,
598
668
  },
599
669
  merchandising: {
600
- personalized: store.merchandising.personalized,
601
- redirect: store.merchandising.redirect,
602
- triggeredCampaigns: (store.merchandising.campaigns?.length &&
603
- store.merchandising.campaigns?.map((campaign) => {
604
- 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);
605
675
  return {
606
676
  id: campaign.id,
607
677
  experimentId: experiement?.experimentId,
@@ -611,12 +681,12 @@ function getSearchSchemaData({ params, store, results, }) {
611
681
  undefined,
612
682
  },
613
683
  results: results?.map((result) => {
614
- const core = result.mappings.core;
684
+ const core = result.mappings?.core;
685
+ const position = result.position;
615
686
  return {
687
+ position,
616
688
  uid: core.uid || '',
617
- // childUid: core.uid,
618
689
  sku: core.sku,
619
- // childSku: core.sku,
620
690
  };
621
691
  }) || [],
622
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.1",
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.1",
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.1",
29
- "@searchspring/snap-event-manager": "^0.65.1",
30
- "@searchspring/snap-logger": "^0.65.1",
31
- "@searchspring/snap-profiler": "^0.65.1",
32
- "@searchspring/snap-store-mobx": "^0.65.1",
33
- "@searchspring/snap-tracker": "^0.65.1",
34
- "@searchspring/snap-url-manager": "^0.65.1"
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": "cd33cd87ad8d51302000b31bce6aa90b4b25ba89"
40
+ "gitHead": "97d209ef85cfb63946bea8ce3737b7dcb2803b89"
41
41
  }