@searchspring/snap-controller 0.73.7 → 0.75.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 +0 -1
  2. package/dist/cjs/Autocomplete/AutocompleteController.d.ts.map +1 -1
  3. package/dist/cjs/Autocomplete/AutocompleteController.js +124 -68
  4. package/dist/cjs/Finder/FinderController.d.ts.map +1 -1
  5. package/dist/cjs/Finder/FinderController.js +3 -0
  6. package/dist/cjs/Recommendation/RecommendationController.d.ts.map +1 -1
  7. package/dist/cjs/Recommendation/RecommendationController.js +84 -58
  8. package/dist/cjs/Search/SearchController.d.ts.map +1 -1
  9. package/dist/cjs/Search/SearchController.js +142 -66
  10. package/dist/cjs/utils/isClickWithinProductLink.d.ts +1 -1
  11. package/dist/cjs/utils/isClickWithinProductLink.d.ts.map +1 -1
  12. package/dist/cjs/utils/isClickWithinProductLink.js +14 -12
  13. package/dist/esm/Autocomplete/AutocompleteController.d.ts +0 -1
  14. package/dist/esm/Autocomplete/AutocompleteController.d.ts.map +1 -1
  15. package/dist/esm/Autocomplete/AutocompleteController.js +101 -28
  16. package/dist/esm/Finder/FinderController.d.ts.map +1 -1
  17. package/dist/esm/Finder/FinderController.js +3 -0
  18. package/dist/esm/Recommendation/RecommendationController.d.ts.map +1 -1
  19. package/dist/esm/Recommendation/RecommendationController.js +64 -22
  20. package/dist/esm/Search/SearchController.d.ts.map +1 -1
  21. package/dist/esm/Search/SearchController.js +106 -22
  22. package/dist/esm/utils/isClickWithinProductLink.d.ts +1 -1
  23. package/dist/esm/utils/isClickWithinProductLink.d.ts.map +1 -1
  24. package/dist/esm/utils/isClickWithinProductLink.js +13 -12
  25. package/package.json +10 -10
@@ -11,6 +11,9 @@ const BACKGROUND_FILTERS_VALUE_FLAGS = [1, 0, '1', '0', 'true', 'false', true, f
11
11
  const defaultConfig = {
12
12
  id: 'search',
13
13
  globals: {},
14
+ beacon: {
15
+ enabled: true,
16
+ },
14
17
  settings: {
15
18
  redirects: {
16
19
  merchandising: true,
@@ -36,7 +39,15 @@ export class SearchController extends AbstractController {
36
39
  this.track = {
37
40
  banner: {
38
41
  impression: ({ uid, responseId }) => {
39
- if (this.events[responseId]?.banner[uid]?.impression) {
42
+ if (!uid) {
43
+ this.log.warn('No banner provided to track.banner.impression');
44
+ return;
45
+ }
46
+ if (!this.events[responseId]) {
47
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
48
+ return;
49
+ }
50
+ else if (this.events[responseId]?.banner[uid]?.impression) {
40
51
  return;
41
52
  }
42
53
  const banner = { uid };
@@ -46,12 +57,20 @@ export class SearchController extends AbstractController {
46
57
  results: [],
47
58
  };
48
59
  this.eventManager.fire('track.banner.impression', { controller: this, product: { uid }, trackEvent: data });
49
- this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
60
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
50
61
  this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
51
62
  this.events[responseId].banner[uid].impression = true;
52
63
  },
53
64
  click: (e, banner) => {
65
+ if (!banner) {
66
+ this.log.warn('No banner provided to track.banner.click');
67
+ return;
68
+ }
54
69
  const { responseId, uid } = banner;
70
+ if (!this.events[responseId]) {
71
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
72
+ return;
73
+ }
55
74
  if (isClickWithinBannerLink(e)) {
56
75
  if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
57
76
  return;
@@ -65,13 +84,21 @@ export class SearchController extends AbstractController {
65
84
  }
66
85
  },
67
86
  clickThrough: (e, { uid, responseId }) => {
87
+ if (!uid) {
88
+ this.log.warn('No banner provided to track.banner.clickThrough');
89
+ return;
90
+ }
91
+ if (!this.events[responseId]) {
92
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
93
+ return;
94
+ }
68
95
  const banner = { uid };
69
96
  const data = {
70
97
  responseId,
71
98
  banners: [banner],
72
99
  };
73
100
  this.eventManager.fire('track.banner.clickThrough', { controller: this, event: e, product: { uid }, trackEvent: data });
74
- this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
101
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
75
102
  this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
76
103
  this.events[responseId].banner[uid].clickThrough = true;
77
104
  setTimeout(() => {
@@ -81,7 +108,15 @@ export class SearchController extends AbstractController {
81
108
  },
82
109
  product: {
83
110
  clickThrough: (e, result) => {
111
+ if (!result) {
112
+ this.log.warn('No result provided to track.product.clickThrough');
113
+ return;
114
+ }
84
115
  const responseId = result.responseId;
116
+ if (!this.events[responseId]) {
117
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
118
+ return;
119
+ }
85
120
  const target = e.target;
86
121
  const resultHref = result.display?.mappings.core?.url || result.mappings.core?.url || '';
87
122
  const elemHref = target?.getAttribute('href');
@@ -108,21 +143,34 @@ export class SearchController extends AbstractController {
108
143
  }
109
144
  // store position data or empty object
110
145
  this.storage.set('scrollMap', scrollMap);
146
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
111
147
  const item = {
112
- type: result.type,
113
- uid: result.id,
114
- parentId: result.id,
115
- sku: result.mappings.core?.sku,
148
+ type,
149
+ uid: result.id ? '' + result.id : '',
150
+ ...(type === 'product'
151
+ ? {
152
+ parentId: result.id ? '' + result.id : '',
153
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
154
+ }
155
+ : {}),
116
156
  };
117
157
  const data = {
118
158
  responseId,
119
159
  results: [item],
120
160
  };
121
161
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
122
- this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
162
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
123
163
  },
124
164
  click: (e, result) => {
165
+ if (!result) {
166
+ this.log.warn('No result provided to track.product.click');
167
+ return;
168
+ }
125
169
  const responseId = result.responseId;
170
+ if (!this.events[responseId]) {
171
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
172
+ return;
173
+ }
126
174
  if (result.type === 'banner' && isClickWithinBannerLink(e)) {
127
175
  if (this.events?.[responseId]?.product[result.id]?.inlineBannerClickThrough) {
128
176
  return;
@@ -147,15 +195,28 @@ export class SearchController extends AbstractController {
147
195
  }
148
196
  },
149
197
  impression: (result) => {
198
+ if (!result) {
199
+ this.log.warn('No result provided to track.product.impression');
200
+ return;
201
+ }
150
202
  const responseId = result.responseId;
151
- if (this.events[responseId]?.product[result.id]?.impression) {
203
+ if (!this.events[responseId]) {
204
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
152
205
  return;
153
206
  }
207
+ else if (this.events[responseId]?.product[result.id]?.impression) {
208
+ return;
209
+ }
210
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
154
211
  const item = {
155
- type: result.type,
156
- uid: result.id,
157
- parentId: result.id,
158
- sku: result.mappings.core?.sku,
212
+ type,
213
+ uid: result.id ? '' + result.id : '',
214
+ ...(type === 'product'
215
+ ? {
216
+ parentId: result.id ? '' + result.id : '',
217
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
218
+ }
219
+ : {}),
159
220
  };
160
221
  const data = {
161
222
  responseId,
@@ -163,12 +224,20 @@ export class SearchController extends AbstractController {
163
224
  banners: [],
164
225
  };
165
226
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
166
- this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
227
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
167
228
  this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
168
229
  this.events[responseId].product[result.id].impression = true;
169
230
  },
170
231
  addToCart: (result) => {
232
+ if (!result) {
233
+ this.log.warn('No result provided to track.product.addToCart');
234
+ return;
235
+ }
171
236
  const responseId = result.responseId;
237
+ if (!this.events[responseId]) {
238
+ this.log.warn('No responseId found in controller, ensure correct controller is used');
239
+ return;
240
+ }
172
241
  const product = {
173
242
  parentId: result.id,
174
243
  uid: result.id,
@@ -181,16 +250,20 @@ export class SearchController extends AbstractController {
181
250
  results: [product],
182
251
  };
183
252
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
184
- this.tracker.events[this.page.type].addToCart({ data, siteId: this.config.globals?.siteId });
253
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].addToCart({ data, siteId: this.config.globals?.siteId });
185
254
  },
186
255
  },
187
256
  redirect: ({ redirectURL, responseId }) => {
257
+ if (!redirectURL) {
258
+ this.log.warn('No redirectURL provided to track.redirect');
259
+ return;
260
+ }
188
261
  const data = {
189
262
  responseId,
190
263
  redirect: redirectURL,
191
264
  };
192
265
  this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
193
- this.tracker.events.search.redirect({ data, siteId: this.config.globals?.siteId });
266
+ this.config.beacon?.enabled && this.tracker.events.search.redirect({ data, siteId: this.config.globals?.siteId });
194
267
  },
195
268
  };
196
269
  this.search = async () => {
@@ -324,6 +397,8 @@ export class SearchController extends AbstractController {
324
397
  this.previousResults = JSON.parse(JSON.stringify(response.results));
325
398
  // update the store
326
399
  this.store.update(response);
400
+ const data = { responseId: response.tracking.responseId };
401
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].render({ data, siteId: this.config.globals?.siteId });
327
402
  const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
328
403
  try {
329
404
  await this.eventManager.fire('afterStore', {
@@ -392,7 +467,11 @@ export class SearchController extends AbstractController {
392
467
  }
393
468
  };
394
469
  this.addToCart = async (_products) => {
395
- const products = typeof _products.slice == 'function' ? _products.slice() : [_products];
470
+ const products = typeof _products?.slice == 'function' ? _products.slice() : [_products];
471
+ if (!_products || products.length === 0) {
472
+ this.log.warn('No products provided to search controller.addToCart');
473
+ return;
474
+ }
396
475
  products.forEach((product) => {
397
476
  this.track.product.addToCart(product);
398
477
  });
@@ -514,10 +593,7 @@ export class SearchController extends AbstractController {
514
593
  this.eventManager.on('afterStore', async (search, next) => {
515
594
  await next();
516
595
  const controller = search.controller;
517
- const responseId = search.response.tracking.responseId;
518
596
  if (controller.store.loaded && !controller.store.error) {
519
- const data = { responseId };
520
- this.tracker.events[this.page.type].render({ data, siteId: this.config.globals?.siteId });
521
597
  const config = search.controller.config;
522
598
  const nonBackgroundFilters = search?.request?.filters?.filter((filter) => !filter.background);
523
599
  if (config?.settings?.redirects?.singleResult &&
@@ -666,8 +742,16 @@ export function generateHrefSelector(element, href, levels = 7) {
666
742
  let level = 0;
667
743
  let elem = element;
668
744
  while (elem && level <= levels) {
669
- // check within
670
- const innerHrefElem = elem.querySelector(`[href*="${href}"]`);
745
+ let innerHrefElem = null;
746
+ try {
747
+ innerHrefElem = elem.querySelector(`[href*="${href}"]`);
748
+ }
749
+ catch (e) {
750
+ try {
751
+ innerHrefElem = elem.querySelector(cssEscape(`[href*="${href}"]`));
752
+ }
753
+ catch { }
754
+ }
671
755
  if (innerHrefElem) {
672
756
  // innerHrefElem was found! now get selectors up to elem that contained it
673
757
  let selector = '';
@@ -1,5 +1,5 @@
1
1
  import type { Product } from '@searchspring/snap-store-mobx';
2
- export declare const CLICK_DUPLICATION_TIMEOUT = 300;
2
+ export declare const CLICK_DUPLICATION_TIMEOUT = 1000;
3
3
  export declare const CLICK_THROUGH_CLOSEST_MAX_LEVELS = 12;
4
4
  export declare const isClickWithinProductLink: (e: MouseEvent, result: Product) => boolean;
5
5
  //# sourceMappingURL=isClickWithinProductLink.d.ts.map
@@ -1 +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,OAgBzE,CAAC"}
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,OAAO,CAAC;AAC9C,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD,eAAO,MAAM,wBAAwB,MAAO,UAAU,UAAU,OAAO,KAAG,OAoBzE,CAAC"}
@@ -1,18 +1,19 @@
1
- export const CLICK_DUPLICATION_TIMEOUT = 300;
1
+ export const CLICK_DUPLICATION_TIMEOUT = 1000;
2
2
  export const CLICK_THROUGH_CLOSEST_MAX_LEVELS = 12;
3
3
  export const isClickWithinProductLink = (e, result) => {
4
- let currentElement = e.target;
5
- let href = null;
6
- let level = 0;
7
- const resultCoreUrl = result?.display?.mappings.core?.url || '';
8
- const resultDisplayUrl = result?.mappings.core?.url || '';
9
- while (currentElement && level < CLICK_THROUGH_CLOSEST_MAX_LEVELS) {
10
- href = currentElement.getAttribute('href');
11
- if (href && ((resultCoreUrl && href.includes(resultCoreUrl)) || (resultDisplayUrl && href.includes(resultDisplayUrl)))) {
12
- return true;
4
+ const resultDisplayUrl = result?.display?.mappings.core?.url || '';
5
+ const resultCoreUrl = result?.mappings.core?.url || '';
6
+ // Get the composed path to handle shadow DOM elements
7
+ const path = e.composedPath ? e.composedPath() : [e.target];
8
+ // Check up to CLICK_THROUGH_CLOSEST_MAX_LEVELS elements in the path
9
+ const elementsToCheck = path.slice(0, CLICK_THROUGH_CLOSEST_MAX_LEVELS);
10
+ for (const element of elementsToCheck) {
11
+ if (element instanceof Element) {
12
+ const href = element.getAttribute('href');
13
+ if (href && ((resultCoreUrl && href.includes(resultCoreUrl)) || (resultDisplayUrl && href.includes(resultDisplayUrl)))) {
14
+ return true;
15
+ }
13
16
  }
14
- currentElement = currentElement.parentElement;
15
- level++;
16
17
  }
17
18
  return false;
18
19
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searchspring/snap-controller",
3
- "version": "0.73.7",
3
+ "version": "0.75.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.73.7",
23
+ "@searchspring/snap-toolbox": "0.75.0",
24
24
  "css.escape": "1.5.1",
25
25
  "deepmerge": "4.3.1"
26
26
  },
27
27
  "devDependencies": {
28
- "@searchspring/snap-client": "0.73.7",
29
- "@searchspring/snap-event-manager": "0.73.7",
30
- "@searchspring/snap-logger": "0.73.7",
31
- "@searchspring/snap-profiler": "0.73.7",
32
- "@searchspring/snap-store-mobx": "0.73.7",
33
- "@searchspring/snap-tracker": "0.73.7",
34
- "@searchspring/snap-url-manager": "0.73.7"
28
+ "@searchspring/snap-client": "0.75.0",
29
+ "@searchspring/snap-event-manager": "0.75.0",
30
+ "@searchspring/snap-logger": "0.75.0",
31
+ "@searchspring/snap-profiler": "0.75.0",
32
+ "@searchspring/snap-store-mobx": "0.75.0",
33
+ "@searchspring/snap-tracker": "0.75.0",
34
+ "@searchspring/snap-url-manager": "0.75.0"
35
35
  },
36
36
  "sideEffects": false,
37
37
  "files": [
38
38
  "dist/**/*"
39
39
  ],
40
- "gitHead": "8da3a6b990f69a5cea07aa4570db108a83952f4c"
40
+ "gitHead": "262c02e06e2bc4637e8e4eb9e48990cc3a9d3c18"
41
41
  }