@athoscommerce/snap-preact 1.10.0 → 1.11.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 (109) hide show
  1. package/dist/cjs/components/src/components/Atoms/TemplateSelect/TemplateSelect.js +5 -1
  2. package/dist/cjs/components/src/components/Molecules/Grid/Grid.js +9 -13
  3. package/dist/cjs/components/src/components/Molecules/List/List.js +9 -13
  4. package/dist/cjs/components/src/components/Molecules/RadioList/RadioList.js +9 -13
  5. package/dist/cjs/components/src/components/Molecules/Select/Select.js +9 -13
  6. package/dist/cjs/components/src/components/Molecules/TabSelection/TabSelection.js +108 -0
  7. package/dist/cjs/components/src/components/Molecules/TabSelection/index.js +17 -0
  8. package/dist/cjs/components/src/components/Molecules/TemplatesEditor/TemplatesEditor.js +1 -1
  9. package/dist/cjs/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.js +17 -2
  10. package/dist/cjs/components/src/components/Organisms/Layout/Layout.js +15 -1
  11. package/dist/cjs/components/src/components/Organisms/Results/Results.js +3 -3
  12. package/dist/cjs/components/src/components/Organisms/Toolbar/Toolbar.js +2 -1
  13. package/dist/cjs/components/src/components/Templates/AutocompleteFixed/AutocompleteFixed.js +10 -3
  14. package/dist/cjs/components/src/components/Templates/AutocompleteModal/AutocompleteModal.js +10 -3
  15. package/dist/cjs/components/src/components/Templates/AutocompleteSlideout/AutocompleteSlideout.js +10 -3
  16. package/dist/cjs/components/src/components/Templates/Search/Search.js +10 -3
  17. package/dist/cjs/components/src/hooks/useAcRenderedInput.js +14 -3
  18. package/dist/cjs/components/src/hooks/useCreateController.js +24 -3
  19. package/dist/cjs/components/src/index.js +1 -0
  20. package/dist/cjs/components/src/providers/themeComponents.js +1 -0
  21. package/dist/cjs/components/src/utilities/index.js +1 -0
  22. package/dist/cjs/components/src/utilities/selectionKey.js +12 -0
  23. package/dist/cjs/src/Templates/SnapTemplates.js +143 -32
  24. package/dist/cjs/src/Templates/Stores/LibraryStore.js +2 -0
  25. package/dist/cjs/src/Templates/Stores/TabManagerStore.js +151 -0
  26. package/dist/cjs/src/Templates/Stores/TemplateStore.js +22 -0
  27. package/dist/cjs/src/Templates/Stores/library/languages/es.js +12 -0
  28. package/dist/cjs/src/Templates/Stores/library/languages/fr.js +12 -0
  29. package/dist/cjs/src/create/createAutocompleteController.js +6 -1
  30. package/dist/esm/components/src/components/Atoms/TemplateSelect/TemplateSelect.d.ts.map +1 -1
  31. package/dist/esm/components/src/components/Atoms/TemplateSelect/TemplateSelect.js +5 -1
  32. package/dist/esm/components/src/components/Molecules/Grid/Grid.d.ts.map +1 -1
  33. package/dist/esm/components/src/components/Molecules/Grid/Grid.js +10 -14
  34. package/dist/esm/components/src/components/Molecules/List/List.d.ts.map +1 -1
  35. package/dist/esm/components/src/components/Molecules/List/List.js +10 -14
  36. package/dist/esm/components/src/components/Molecules/RadioList/RadioList.d.ts.map +1 -1
  37. package/dist/esm/components/src/components/Molecules/RadioList/RadioList.js +10 -14
  38. package/dist/esm/components/src/components/Molecules/Select/Select.d.ts.map +1 -1
  39. package/dist/esm/components/src/components/Molecules/Select/Select.js +10 -14
  40. package/dist/esm/components/src/components/Molecules/TabSelection/TabSelection.d.ts +32 -0
  41. package/dist/esm/components/src/components/Molecules/TabSelection/TabSelection.d.ts.map +1 -0
  42. package/dist/esm/components/src/components/Molecules/TabSelection/TabSelection.js +102 -0
  43. package/dist/esm/components/src/components/Molecules/TabSelection/index.d.ts +2 -0
  44. package/dist/esm/components/src/components/Molecules/TabSelection/index.d.ts.map +1 -0
  45. package/dist/esm/components/src/components/Molecules/TabSelection/index.js +1 -0
  46. package/dist/esm/components/src/components/Molecules/TemplatesEditor/TemplatesEditor.d.ts.map +1 -1
  47. package/dist/esm/components/src/components/Molecules/TemplatesEditor/TemplatesEditor.js +1 -1
  48. package/dist/esm/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.d.ts +3 -1
  49. package/dist/esm/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.d.ts.map +1 -1
  50. package/dist/esm/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.js +17 -2
  51. package/dist/esm/components/src/components/Organisms/Layout/Layout.d.ts +3 -1
  52. package/dist/esm/components/src/components/Organisms/Layout/Layout.d.ts.map +1 -1
  53. package/dist/esm/components/src/components/Organisms/Layout/Layout.js +15 -1
  54. package/dist/esm/components/src/components/Organisms/Results/Results.d.ts.map +1 -1
  55. package/dist/esm/components/src/components/Organisms/Results/Results.js +3 -3
  56. package/dist/esm/components/src/components/Organisms/Toolbar/Toolbar.d.ts +3 -1
  57. package/dist/esm/components/src/components/Organisms/Toolbar/Toolbar.d.ts.map +1 -1
  58. package/dist/esm/components/src/components/Organisms/Toolbar/Toolbar.js +2 -1
  59. package/dist/esm/components/src/components/Templates/AutocompleteFixed/AutocompleteFixed.d.ts +2 -0
  60. package/dist/esm/components/src/components/Templates/AutocompleteFixed/AutocompleteFixed.d.ts.map +1 -1
  61. package/dist/esm/components/src/components/Templates/AutocompleteFixed/AutocompleteFixed.js +10 -3
  62. package/dist/esm/components/src/components/Templates/AutocompleteModal/AutocompleteModal.d.ts +2 -0
  63. package/dist/esm/components/src/components/Templates/AutocompleteModal/AutocompleteModal.d.ts.map +1 -1
  64. package/dist/esm/components/src/components/Templates/AutocompleteModal/AutocompleteModal.js +10 -3
  65. package/dist/esm/components/src/components/Templates/AutocompleteSlideout/AutocompleteSlideout.d.ts +2 -0
  66. package/dist/esm/components/src/components/Templates/AutocompleteSlideout/AutocompleteSlideout.d.ts.map +1 -1
  67. package/dist/esm/components/src/components/Templates/AutocompleteSlideout/AutocompleteSlideout.js +10 -3
  68. package/dist/esm/components/src/components/Templates/Search/Search.d.ts +2 -0
  69. package/dist/esm/components/src/components/Templates/Search/Search.d.ts.map +1 -1
  70. package/dist/esm/components/src/components/Templates/Search/Search.js +10 -3
  71. package/dist/esm/components/src/hooks/useAcRenderedInput.d.ts +2 -2
  72. package/dist/esm/components/src/hooks/useAcRenderedInput.d.ts.map +1 -1
  73. package/dist/esm/components/src/hooks/useAcRenderedInput.js +15 -4
  74. package/dist/esm/components/src/hooks/useCreateController.d.ts.map +1 -1
  75. package/dist/esm/components/src/hooks/useCreateController.js +24 -3
  76. package/dist/esm/components/src/index.d.ts +1 -0
  77. package/dist/esm/components/src/index.d.ts.map +1 -1
  78. package/dist/esm/components/src/index.js +1 -0
  79. package/dist/esm/components/src/providers/langComponents.d.ts +2 -0
  80. package/dist/esm/components/src/providers/langComponents.d.ts.map +1 -1
  81. package/dist/esm/components/src/providers/themeComponents.d.ts +41 -7
  82. package/dist/esm/components/src/providers/themeComponents.d.ts.map +1 -1
  83. package/dist/esm/components/src/providers/themeComponents.js +1 -0
  84. package/dist/esm/components/src/utilities/index.d.ts +1 -0
  85. package/dist/esm/components/src/utilities/index.d.ts.map +1 -1
  86. package/dist/esm/components/src/utilities/index.js +1 -0
  87. package/dist/esm/components/src/utilities/selectionKey.d.ts +3 -0
  88. package/dist/esm/components/src/utilities/selectionKey.d.ts.map +1 -0
  89. package/dist/esm/components/src/utilities/selectionKey.js +9 -0
  90. package/dist/esm/src/Templates/SnapTemplates.d.ts +6 -3
  91. package/dist/esm/src/Templates/SnapTemplates.d.ts.map +1 -1
  92. package/dist/esm/src/Templates/SnapTemplates.js +141 -31
  93. package/dist/esm/src/Templates/Stores/LibraryStore.d.ts +2 -0
  94. package/dist/esm/src/Templates/Stores/LibraryStore.d.ts.map +1 -1
  95. package/dist/esm/src/Templates/Stores/LibraryStore.js +2 -0
  96. package/dist/esm/src/Templates/Stores/TabManagerStore.d.ts +29 -0
  97. package/dist/esm/src/Templates/Stores/TabManagerStore.d.ts.map +1 -0
  98. package/dist/esm/src/Templates/Stores/TabManagerStore.js +146 -0
  99. package/dist/esm/src/Templates/Stores/TemplateStore.d.ts +24 -3
  100. package/dist/esm/src/Templates/Stores/TemplateStore.d.ts.map +1 -1
  101. package/dist/esm/src/Templates/Stores/TemplateStore.js +22 -0
  102. package/dist/esm/src/Templates/Stores/library/languages/es.d.ts.map +1 -1
  103. package/dist/esm/src/Templates/Stores/library/languages/es.js +12 -0
  104. package/dist/esm/src/Templates/Stores/library/languages/fr.d.ts.map +1 -1
  105. package/dist/esm/src/Templates/Stores/library/languages/fr.js +12 -0
  106. package/dist/esm/src/create/createAutocompleteController.js +6 -1
  107. package/dist/esm/src/types.d.ts +6 -1
  108. package/dist/esm/src/types.d.ts.map +1 -1
  109. package/package.json +12 -12
@@ -2,9 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useAcRenderedInput = useAcRenderedInput;
4
4
  const hooks_1 = require("preact/hooks");
5
- function useAcRenderedInput({ input, controller, renderInput, buttonSelector, renderedInputRef, setActive, }) {
5
+ const RENDERED_INPUT_SELECTOR = '.autocomplete__search-input input';
6
+ function useAcRenderedInput({ input, controllers, renderInput, buttonSelector, renderedInputRef, setActive, }) {
6
7
  const [_input, setInput] = (0, hooks_1.useState)(input);
7
8
  const [renderedInputInitialized, setRenderedInputInitialized] = (0, hooks_1.useState)(false);
9
+ const boundController = (0, hooks_1.useRef)(null);
10
+ // adds the rendered input to the controller's selector (once) and binds it
11
+ const bindRenderedInput = async (cntrlrs) => {
12
+ for (const cntrlr of cntrlrs) {
13
+ if (!cntrlr.config.selector.includes(RENDERED_INPUT_SELECTOR)) {
14
+ cntrlr.config.selector = `${cntrlr.config.selector}, ${RENDERED_INPUT_SELECTOR}`;
15
+ }
16
+ boundController.current = cntrlr;
17
+ await cntrlr.bind();
18
+ }
19
+ };
8
20
  const onClick = () => {
9
21
  if (setActive) {
10
22
  setActive(true);
@@ -13,8 +25,7 @@ function useAcRenderedInput({ input, controller, renderInput, buttonSelector, re
13
25
  setTimeout(async () => {
14
26
  if (!renderedInputInitialized) {
15
27
  setInput(renderedInputRef.current);
16
- controller.config.selector = `${controller.config.selector}, .autocomplete__search-input input`;
17
- await controller.bind();
28
+ await bindRenderedInput(controllers);
18
29
  renderedInputRef?.current?.focus();
19
30
  }
20
31
  setRenderedInputInitialized(true);
@@ -11,10 +11,31 @@ const useCreateController = (snap, type, config) => {
11
11
  .then((controller) => {
12
12
  setController(controller);
13
13
  })
14
- .catch(() => {
15
- snap.createController(type, config).then((controller) => {
14
+ .catch(async () => {
15
+ let configToCreate = config;
16
+ // dynamically created recommendation controllers (ex. no-results recommendations) don't go through
17
+ // the RecommendationInstantiator, so inherit its plugins/middleware from the original config to keep behavior consistent
18
+ if (type === 'recommendation' && !config.plugins && !config.middleware) {
19
+ try {
20
+ const instantiator = await snap.getInstantiator('recommendation');
21
+ configToCreate = {
22
+ ...config,
23
+ plugins: instantiator.config.config.plugins,
24
+ middleware: instantiator.config.config.middleware,
25
+ };
26
+ }
27
+ catch {
28
+ // no recommendation instantiator configured - proceed without inherited attachments
29
+ }
30
+ }
31
+ try {
32
+ const controller = await snap.createController(type, configToCreate);
16
33
  setController(controller);
17
- });
34
+ }
35
+ catch (err) {
36
+ // eslint-disable-next-line no-console
37
+ console.error(`useCreateController: failed to create controller "${config.id}"`, err);
38
+ }
18
39
  });
19
40
  }, []);
20
41
  return controller;
@@ -63,6 +63,7 @@ __exportStar(require("./components/Molecules/SearchInput"), exports);
63
63
  __exportStar(require("./components/Molecules/Select"), exports);
64
64
  __exportStar(require("./components/Molecules/Slideout"), exports);
65
65
  __exportStar(require("./components/Molecules/SortBy"), exports);
66
+ __exportStar(require("./components/Molecules/TabSelection"), exports);
66
67
  __exportStar(require("./components/Molecules/TemplatesEditor"), exports);
67
68
  __exportStar(require("./components/Molecules/Swatches"), exports);
68
69
  __exportStar(require("./components/Molecules/Slideshow"), exports);
@@ -51,6 +51,7 @@ exports.ALL_CUSTOM_COMPONENT_TYPES = [
51
51
  'slideout',
52
52
  'sortBy',
53
53
  'swatches',
54
+ 'tabSelection',
54
55
  'variantSelection',
55
56
  'terms',
56
57
  /* organisms */
@@ -24,6 +24,7 @@ Object.defineProperty(exports, "Colour", { enumerable: true, get: function () {
24
24
  __exportStar(require("./parseProps"), exports);
25
25
  __exportStar(require("./mergeProps"), exports);
26
26
  __exportStar(require("./mergeStyles"), exports);
27
+ __exportStar(require("./selectionKey"), exports);
27
28
  __exportStar(require("./storybook"), exports);
28
29
  __exportStar(require("./defaultBadgeComponentMap"), exports);
29
30
  __exportStar(require("./createImpressionObserver"), exports);
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectionKey = selectionKey;
4
+ // identifies a selection by the fields that options are matched on when rendering, so that a store
5
+ // handing back an equivalent - but not identical - option counts as the same selection. option
6
+ // objects themselves are not comparable: those built by the stores carry a UrlManager that picks up
7
+ // every url change, which would read as a new selection on each navigation
8
+ function selectionKey(selected) {
9
+ const options = selected == undefined ? [] : Array.isArray(selected) ? selected : [selected];
10
+ // label is included so that options without a value still produce distinct keys
11
+ return options.map((option) => `${option?.value}:${option?.label}`).join('|');
12
+ }
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createSearchTargeters = exports.SnapTemplates = exports.applyAutomaticThemeOverrides = exports.DEFAULT_AUTOCOMPLETE_CONTROLLER_SETTINGS = exports.DEFAULT_FEATURES = exports.TEMPLATE_EDITOR_UI_PARAM = exports.TEMPLATE_EDITOR_COOKIE = void 0;
6
+ exports.createSearchTargeters = exports.SnapTemplates = exports.applyAutomaticThemeOverrides = exports.TAB_PREFIXED_PARAMS = exports.DEFAULT_AUTOCOMPLETE_CONTROLLER_SETTINGS = exports.DEFAULT_FEATURES = exports.TEMPLATE_EDITOR_UI_PARAM = exports.TEMPLATE_EDITOR_COOKIE = void 0;
7
7
  exports.mapBreakpoints = mapBreakpoints;
8
8
  exports.createAutocompleteTargeters = createAutocompleteTargeters;
9
9
  exports.createRecommendationComponentMapping = createRecommendationComponentMapping;
10
10
  exports.createSnapConfig = createSnapConfig;
11
11
  exports.createPlugins = createPlugins;
12
+ exports.createTabPlugins = createTabPlugins;
12
13
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
13
14
  const preact_1 = require("preact");
14
15
  const deepmerge_1 = __importDefault(require("deepmerge"));
@@ -16,6 +17,9 @@ const Snap_1 = require("../Snap");
16
17
  const TemplateSelect_1 = require("../../components/src/components/Atoms/TemplateSelect");
17
18
  const snap_toolbox_1 = require("@athoscommerce/snap-toolbox");
18
19
  const TemplateStore_1 = require("./Stores/TemplateStore");
20
+ const TabManagerStore_1 = require("./Stores/TabManagerStore");
21
+ const snap_client_1 = require("@athoscommerce/snap-client");
22
+ const snap_tracker_1 = require("@athoscommerce/snap-tracker");
19
23
  const shopify_1 = require("@athoscommerce/snap-platforms/shopify");
20
24
  const utils_1 = require("../utils");
21
25
  exports.TEMPLATE_EDITOR_COOKIE = 'athosEditor';
@@ -26,6 +30,8 @@ exports.DEFAULT_AUTOCOMPLETE_CONTROLLER_SETTINGS = {
26
30
  limit: 5,
27
31
  },
28
32
  };
33
+ // tabs namespace their catalog specific url state - the query is shared so a single search spans every tab
34
+ exports.TAB_PREFIXED_PARAMS = ['filter', 'sort', 'pageSize', 'rq', 'page'];
29
35
  const hasShopifyMarketsPluginConfig = (templateConfig) => {
30
36
  const pluginConfigs = [
31
37
  templateConfig.plugins?.shopify?.markets,
@@ -105,8 +111,8 @@ class SnapTemplates extends Snap_1.Snap {
105
111
  // attach to the window object
106
112
  window.athos = window.athos || {};
107
113
  window.athos.editor = templateEditorStore;
108
- const searchController = this.controllers['search'];
109
- const autocompleteController = this.controllers['autocomplete'];
114
+ const searchController = this.getTemplateController('search');
115
+ const autocompleteController = this.getTemplateController('autocomplete');
110
116
  if (searchController) {
111
117
  templateEditorStore.registerController(searchController);
112
118
  }
@@ -152,6 +158,16 @@ class SnapTemplates extends Snap_1.Snap {
152
158
  }
153
159
  });
154
160
  }
161
+ // when tabs are configured there is no controller using the `search` / `autocomplete` id -
162
+ // the default tab controller is used instead
163
+ getTemplateController(type) {
164
+ const tabs = this.templates.config[type]?.tabs;
165
+ if (tabs?.length) {
166
+ const defaultTab = tabs.filter((tab) => tab.default)[0] || tabs[0];
167
+ return this.controllers[defaultTab.id];
168
+ }
169
+ return this.controllers[type];
170
+ }
155
171
  }
156
172
  exports.SnapTemplates = SnapTemplates;
157
173
  function mapBreakpoints(breakpointsKeys, breakpointSettings) {
@@ -277,6 +293,16 @@ function createRecommendationComponentMapping(templateConfig, templatesStore) {
277
293
  return mapping;
278
294
  }, {});
279
295
  }
296
+ function createTabServices(snapConfig, siteId) {
297
+ const tabGlobals = {
298
+ ...snapConfig.client?.globals,
299
+ siteId,
300
+ };
301
+ return {
302
+ client: new snap_client_1.Client(tabGlobals, snapConfig.client?.config),
303
+ tracker: new snap_tracker_1.Tracker(tabGlobals, snapConfig.tracker?.config),
304
+ };
305
+ }
280
306
  function createSnapConfig(templateConfig, templatesStore) {
281
307
  const initiatorPrefix = window?.athos?.managed ? `managed/` : '';
282
308
  const snapConfig = {
@@ -307,37 +333,63 @@ function createSnapConfig(templateConfig, templatesStore) {
307
333
  }
308
334
  /* SEARCH CONTROLLER */
309
335
  if (templateConfig.search && snapConfig.controllers) {
310
- const searchControllerConfig = {
311
- config: {
312
- id: 'search',
313
- plugins: createPlugins(templateConfig, templatesStore, 'search'),
314
- globals: templateConfig.search.globals || {},
315
- settings: templateConfig.search.settings || {},
316
- },
317
- targeters: (0, exports.createSearchTargeters)(templateConfig, templatesStore),
318
- };
319
- // merge the responsive settings if there are any
320
- // if (templateConfig.config.theme.variables?.breakpoints && templateConfig.search.breakpointSettings) {
321
- // const mappedBreakpoints = mapBreakpoints(templateConfig.config.theme.variables.breakpoints, templateConfig.search.breakpointSettings || []);
322
- // const breakpointSettings = getDisplaySettings(mappedBreakpoints) as SearchStoreConfigSettings;
323
- // searchControllerConfig.config.settings = deepmerge(searchControllerConfig.config.settings, breakpointSettings);
324
- // }
325
- snapConfig.controllers.search = [searchControllerConfig];
336
+ const searchTabs = templateConfig.search.tabs || [];
337
+ const searchTargeters = (0, exports.createSearchTargeters)(templateConfig, templatesStore);
338
+ const searchSettings = templateConfig.search.settings || {};
339
+ const searchGlobals = templateConfig.search.globals || {};
340
+ snapConfig.controllers.search = [];
341
+ if (!searchTabs.length) {
342
+ snapConfig.controllers.search.push({
343
+ config: {
344
+ id: 'search',
345
+ plugins: createPlugins(templateConfig, templatesStore, 'search'),
346
+ globals: searchGlobals,
347
+ settings: searchSettings,
348
+ },
349
+ targeters: searchTargeters,
350
+ });
351
+ }
352
+ else {
353
+ // when tabs are configured, the tab controllers do all of the searching - an additional
354
+ // non-tab controller would only produce a redundant API request, so the targeters are
355
+ // attached to the active tab controller instead. targeting kicks off a search before the
356
+ // TabManagerStore exists to gate redirects, so this must be the tab the shopper is viewing
357
+ const targetedTab = (0, TabManagerStore_1.getActiveTabConfig)(searchTabs) || searchTabs[0];
358
+ searchTabs.forEach((tab) => {
359
+ const tabParam = tab.param;
360
+ snapConfig.controllers.search.push({
361
+ url: {
362
+ settings: {
363
+ // the query is shared across tabs - only per-catalog state is namespaced
364
+ ...(tabParam ? { corePrefix: tabParam, corePrefixParams: exports.TAB_PREFIXED_PARAMS } : {}),
365
+ },
366
+ parameters: {
367
+ custom: {
368
+ [TabManagerStore_1.TAB_ID_DEFAULT_PARAM]: { type: 'query' },
369
+ },
370
+ },
371
+ },
372
+ config: {
373
+ id: tab.id,
374
+ plugins: tab.plugins ? createTabPlugins(tab.plugins, templatesStore) : createPlugins(templateConfig, templatesStore, 'search'),
375
+ settings: {
376
+ ...(0, deepmerge_1.default)(searchSettings, tab.settings || {}),
377
+ },
378
+ globals: (0, deepmerge_1.default)(searchGlobals, tab.globals || {}),
379
+ },
380
+ services: createTabServices(snapConfig, tab.siteId),
381
+ ...(tab === targetedTab ? { targeters: searchTargeters } : {}),
382
+ });
383
+ });
384
+ }
326
385
  }
327
386
  /* AUTOCOMPLETE CONTROLLER */
328
387
  if (templateConfig.autocomplete && snapConfig.controllers) {
329
388
  const autocompleteControllerSettings = (0, deepmerge_1.default)(exports.DEFAULT_AUTOCOMPLETE_CONTROLLER_SETTINGS, templateConfig.autocomplete.settings || {});
330
- const autocompleteControllerConfig = {
331
- config: {
332
- id: 'autocomplete',
333
- plugins: createPlugins(templateConfig, templatesStore, 'autocomplete'),
334
- selector: templateConfig.autocomplete.targets.map((target) => target.inputSelector).join(', '),
335
- action: templateConfig.autocomplete.action || '',
336
- globals: templateConfig.autocomplete.globals || {},
337
- settings: autocompleteControllerSettings,
338
- },
339
- targeters: createAutocompleteTargeters(templateConfig, templatesStore),
340
- };
389
+ const autocompleteSelector = templateConfig.autocomplete.targets.map((target) => target.inputSelector).join(', ');
390
+ const autocompleteTabs = templateConfig.autocomplete.tabs || [];
391
+ const autocompleteGlobals = templateConfig.autocomplete.globals || {};
392
+ const autocompleteTargeters = createAutocompleteTargeters(templateConfig, templatesStore);
341
393
  // merge the responsive settings if there are any
342
394
  // if (templateConfig.config.theme.variables?.breakpoints && templateConfig.autocomplete.breakpointSettings) {
343
395
  // const mappedBreakpoints = mapBreakpoints(
@@ -345,9 +397,61 @@ function createSnapConfig(templateConfig, templatesStore) {
345
397
  // templateConfig.autocomplete.breakpointSettings || []
346
398
  // );
347
399
  // const breakpointSettings = getDisplaySettings(mappedBreakpoints) as AutocompleteStoreConfigSettings;
348
- // autocompleteControllerConfig.config.settings = deepmerge(autocompleteControllerConfig.config.settings, breakpointSettings);
400
+ // autocompleteControllerSettings = deepmerge(autocompleteControllerSettings, breakpointSettings);
349
401
  // }
350
- snapConfig.controllers.autocomplete = [autocompleteControllerConfig];
402
+ snapConfig.controllers.autocomplete = [];
403
+ if (!autocompleteTabs.length) {
404
+ snapConfig.controllers.autocomplete.push({
405
+ config: {
406
+ id: 'autocomplete',
407
+ plugins: createPlugins(templateConfig, templatesStore, 'autocomplete'),
408
+ selector: autocompleteSelector,
409
+ action: templateConfig.autocomplete.action || '',
410
+ globals: autocompleteGlobals,
411
+ settings: autocompleteControllerSettings,
412
+ },
413
+ targeters: autocompleteTargeters,
414
+ });
415
+ }
416
+ else {
417
+ // when tabs are configured, every tab controller binds to the same input - an additional
418
+ // non-tab controller would only produce redundant API requests, so the targeters are
419
+ // attached to the default tab controller instead
420
+ const defaultTab = autocompleteTabs.filter((tab) => tab.default)[0] || autocompleteTabs[0];
421
+ autocompleteTabs.forEach((tab) => {
422
+ const tabParam = tab.param;
423
+ snapConfig.controllers.autocomplete.push({
424
+ url: {
425
+ settings: {
426
+ // the query is shared across tabs - only per-catalog state is namespaced
427
+ ...(tabParam ? { corePrefix: tabParam, corePrefixParams: exports.TAB_PREFIXED_PARAMS } : {}),
428
+ },
429
+ parameters: {
430
+ custom: {
431
+ [TabManagerStore_1.TAB_ID_DEFAULT_PARAM]: { type: 'query' },
432
+ },
433
+ },
434
+ // every url the autocomplete builds must carry the tab it belongs to
435
+ globals: [
436
+ {
437
+ param: TabManagerStore_1.TAB_ID_DEFAULT_PARAM,
438
+ value: tab.param,
439
+ },
440
+ ],
441
+ },
442
+ config: {
443
+ id: tab.id,
444
+ plugins: tab.plugins ? createTabPlugins(tab.plugins, templatesStore) : createPlugins(templateConfig, templatesStore, 'autocomplete'),
445
+ selector: autocompleteSelector,
446
+ action: templateConfig.autocomplete.action || '',
447
+ settings: (0, deepmerge_1.default)(autocompleteControllerSettings, tab.settings || {}),
448
+ globals: (0, deepmerge_1.default)(autocompleteGlobals, tab.globals || {}),
449
+ },
450
+ services: createTabServices(snapConfig, tab.siteId),
451
+ ...(tab === defaultTab ? { targeters: autocompleteTargeters } : {}),
452
+ });
453
+ });
454
+ }
351
455
  }
352
456
  /* RECOMMENDATION INSTANTIATOR */
353
457
  const originalRecsConfig = templateConfig.recommendation || {};
@@ -488,3 +592,10 @@ function createPlugins(templateConfig, templatesStore, controllerType) {
488
592
  });
489
593
  return plugins;
490
594
  }
595
+ // tab level plugins do not inherit global or controller level plugins - only the plugins configured on the tab are used
596
+ function createTabPlugins(tabPlugins, templatesStore) {
597
+ const plugins = createPlugins({ plugins: tabPlugins }, templatesStore);
598
+ const customPluginFunctions = Object.values(tabPlugins.custom || {}).map((customPlugin) => customPlugin.function);
599
+ // built-in plugins are always created - keep only the ones that were given configuration on the tab
600
+ return plugins.filter((plugin) => customPluginFunctions.includes(plugin[0]) || Object.keys(plugin[1] || {}).length > 0);
601
+ }
@@ -77,6 +77,7 @@ class LibraryStore {
77
77
  slideout: {},
78
78
  sortBy: {},
79
79
  swatches: {},
80
+ tabSelection: {},
80
81
  variantSelection: {},
81
82
  terms: {},
82
83
  branchOverride: {},
@@ -263,6 +264,7 @@ class LibraryStore {
263
264
  slideout: {},
264
265
  sortBy: {},
265
266
  swatches: {},
267
+ tabSelection: {},
266
268
  variantSelection: {},
267
269
  terms: {},
268
270
  branchOverride: {},
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TabManagerStore = exports.TAB_ID_DEFAULT_PARAM = void 0;
4
+ exports.getActiveTabConfig = getActiveTabConfig;
5
+ const mobx_1 = require("mobx");
6
+ const snap_controller_1 = require("@athoscommerce/snap-controller");
7
+ const snap_toolbox_1 = require("@athoscommerce/snap-toolbox");
8
+ exports.TAB_ID_DEFAULT_PARAM = 'tab';
9
+ // resolves the active tab straight from the url - tab controllers begin searching as soon as they
10
+ // are targeted, which happens before any TabManagerStore exists
11
+ function getActiveTabConfig(tabs) {
12
+ const defaultTab = tabs.filter((tab) => tab.default)[0] || tabs[0];
13
+ const paramValue = (0, snap_toolbox_1.url)(window.location.href)?.params.query[exports.TAB_ID_DEFAULT_PARAM];
14
+ if (!paramValue) {
15
+ return defaultTab;
16
+ }
17
+ const activeParam = decodeURIComponent(paramValue);
18
+ return tabs.filter((tab) => tab.param == activeParam)[0] || defaultTab;
19
+ }
20
+ class TabManagerStore {
21
+ constructor(tabConfigs, controllers) {
22
+ this.tabs = [];
23
+ this.setActive = (id, updateUrl = true) => {
24
+ const tabToSelect = this.getTab(id);
25
+ if (!tabToSelect || tabToSelect.id === this.active?.id) {
26
+ return;
27
+ }
28
+ const previousTab = this.active;
29
+ this.setRedirects(previousTab, false);
30
+ this.active = tabToSelect;
31
+ this.setRedirects(tabToSelect, true);
32
+ if (tabToSelect.controller.type == snap_controller_1.ControllerTypes.search) {
33
+ if (updateUrl) {
34
+ // to remove page chain: `.remove('page')` to urlManager.set(...)
35
+ tabToSelect.controller.urlManager.set(this.param, tabToSelect.param).go();
36
+ }
37
+ this.searchTab(tabToSelect);
38
+ }
39
+ };
40
+ // every tab shares the url and the autocomplete input, so an inactive tab landing on a single
41
+ // result or a merchandising redirect would navigate away from the tab the shopper is viewing
42
+ this.setRedirects = (tab, enabled) => {
43
+ if (!tab) {
44
+ return;
45
+ }
46
+ const config = tab.controller.config;
47
+ config.settings = config.settings || {};
48
+ config.settings.redirects = enabled ? { ...tab.redirects } : { merchandising: false, singleResult: false };
49
+ };
50
+ this.searchTab = (tab) => {
51
+ if (tab.controller.type != snap_controller_1.ControllerTypes.search) {
52
+ return;
53
+ }
54
+ const searchController = tab.controller;
55
+ if (!searchController.store.loading && !searchController.store.loaded) {
56
+ searchController.search();
57
+ }
58
+ };
59
+ this.getTab = (id) => {
60
+ return this.tabs?.filter((tab) => tab.id == id)[0];
61
+ };
62
+ this.getTabByParam = (param) => {
63
+ return this.tabs?.filter((tab) => tab.param == param)[0];
64
+ };
65
+ this.param = exports.TAB_ID_DEFAULT_PARAM;
66
+ const startingTabConfig = tabConfigs.filter((tab) => tab.default)[0] || tabConfigs[0];
67
+ let urlParam;
68
+ tabConfigs.forEach((tabConfig) => {
69
+ const cntrlr = controllers.filter((controller) => controller.id == tabConfig.id)[0];
70
+ if (!cntrlr) {
71
+ return;
72
+ }
73
+ const newTab = {
74
+ id: tabConfig.id,
75
+ label: tabConfig.label,
76
+ siteId: tabConfig.siteId,
77
+ param: tabConfig.param,
78
+ controller: cntrlr,
79
+ // search-tab only setting - autocomplete has nothing to fetch until the shopper types
80
+ prefetch: 'prefetch' in tabConfig ? tabConfig.prefetch ?? true : true,
81
+ redirects: { ...(cntrlr.config.settings?.redirects || {}) },
82
+ };
83
+ if (cntrlr.type == snap_controller_1.ControllerTypes.autocomplete) {
84
+ const autocompleteController = cntrlr;
85
+ // hidden form inputs for tab selections
86
+ if (!autocompleteController.config.action) {
87
+ autocompleteController.eventManager.on('beforeSubmit', async (context, next) => {
88
+ if (context.input?.form) {
89
+ addHiddenFormInput(context.input.form, this.param, this.active?.param || newTab.param);
90
+ }
91
+ await next();
92
+ });
93
+ }
94
+ }
95
+ if (cntrlr.type == snap_controller_1.ControllerTypes.search) {
96
+ const searchController = cntrlr;
97
+ // read the active tab from the url on initial load
98
+ if (!urlParam) {
99
+ urlParam = getTabParamFromUrlState(searchController.urlManager.state[this.param]);
100
+ }
101
+ // functionality for back/forward browser buttons
102
+ searchController.urlManager.subscribe((prev, next) => {
103
+ const nextParam = getTabParamFromUrlState(next?.[this.param]);
104
+ const nextTab = (nextParam && this.getTabByParam(nextParam)) || this.getTab(startingTabConfig.id);
105
+ if (nextTab) {
106
+ this.setActive(nextTab.id, false);
107
+ }
108
+ });
109
+ }
110
+ this.tabs.push(newTab);
111
+ });
112
+ this.active = (urlParam && this.getTabByParam(urlParam)) || this.getTab(startingTabConfig.id) || this.tabs[0];
113
+ (0, mobx_1.makeObservable)(this, {
114
+ tabs: mobx_1.observable,
115
+ active: mobx_1.observable,
116
+ setActive: mobx_1.action,
117
+ });
118
+ // the active tab always searches - the rest only when they opt into prefetching
119
+ this.tabs.forEach((tab) => {
120
+ if (tab.id != this.active?.id) {
121
+ this.setRedirects(tab, false);
122
+ }
123
+ if (tab.prefetch || tab.id == this.active?.id) {
124
+ this.searchTab(tab);
125
+ }
126
+ });
127
+ }
128
+ }
129
+ exports.TabManagerStore = TabManagerStore;
130
+ function addHiddenFormInput(form, name, value) {
131
+ // match by property rather than an interpolated attribute selector to avoid selector injection
132
+ const existingElem = Array.from(form.querySelectorAll('input[type="hidden"]')).find((elem) => elem.name === name);
133
+ if (existingElem) {
134
+ // keep the value current - the active tab can change after the input was first appended
135
+ existingElem.value = value;
136
+ return;
137
+ }
138
+ const inputElem = document.createElement('input');
139
+ inputElem.type = 'hidden';
140
+ inputElem.name = name;
141
+ inputElem.value = value;
142
+ form.append(inputElem);
143
+ }
144
+ // custom url params always deserialize as arrays when parsed fresh from the url, but can be a
145
+ // plain string when read back from an in-session localState set() - handle both shapes
146
+ function getTabParamFromUrlState(value) {
147
+ if (Array.isArray(value)) {
148
+ return value[0];
149
+ }
150
+ return typeof value == 'string' ? value : undefined;
151
+ }
@@ -6,6 +6,7 @@ const mobx_1 = require("mobx");
6
6
  const snap_toolbox_1 = require("@athoscommerce/snap-toolbox");
7
7
  const ThemeStore_1 = require("./ThemeStore");
8
8
  const TargetStore_1 = require("./TargetStore");
9
+ const TabManagerStore_1 = require("./TabManagerStore");
9
10
  const LibraryStore_1 = require("./LibraryStore");
10
11
  const snap_toolbox_2 = require("@athoscommerce/snap-toolbox");
11
12
  const RESIZE_DEBOUNCE = 100;
@@ -14,6 +15,7 @@ class TemplatesStore {
14
15
  constructor(params) {
15
16
  this.loading = false;
16
17
  this.window = { innerWidth: 0 };
18
+ this.tabManagers = {};
17
19
  const { config, settings } = params || {};
18
20
  this.config = config;
19
21
  this.platform = config.config?.platform || 'other';
@@ -124,6 +126,26 @@ class TemplatesStore {
124
126
  getTarget(type, targetIndex) {
125
127
  return getTargetArray(this.targets, type)?.[targetIndex];
126
128
  }
129
+ // one store per controller type
130
+ getTabManager(type, controllers) {
131
+ const tabs = this.config[type]?.tabs;
132
+ if (!tabs || tabs.length < 2) {
133
+ return undefined;
134
+ }
135
+ const mappedTabs = tabs.map((tab) => ({
136
+ id: tab.id,
137
+ param: tab.param,
138
+ siteId: tab.siteId,
139
+ label: tab.label,
140
+ default: tab.default,
141
+ prefetch: tab.prefetch,
142
+ }));
143
+ if (!this.tabManagers[type]) {
144
+ const tabControllers = tabs.map((tab) => controllers[tab.id]).filter((controller) => Boolean(controller));
145
+ this.tabManagers[type] = new TabManagerStore_1.TabManagerStore(mappedTabs, tabControllers);
146
+ }
147
+ return this.tabManagers[type];
148
+ }
127
149
  addTheme(config) {
128
150
  const theme = new ThemeStore_1.ThemeStore({
129
151
  config,
@@ -168,6 +168,18 @@ exports.es = {
168
168
  value: 'Clasificar por',
169
169
  },
170
170
  },
171
+ tabSelection: {
172
+ tabList: {
173
+ attributes: {
174
+ 'aria-label': 'Pestañas de resultados',
175
+ },
176
+ },
177
+ tabButton: {
178
+ attributes: {
179
+ 'aria-label': (data) => `${data.tab.label || data.tab.id}${typeof data.resultCount == 'number' ? `, ${data.resultCount} resultados` : ''}`,
180
+ },
181
+ },
182
+ },
171
183
  facetsHorizontal: {
172
184
  dropdownButton: {
173
185
  attributes: {
@@ -142,6 +142,18 @@ exports.fr = {
142
142
  value: 'Trier Par',
143
143
  },
144
144
  },
145
+ tabSelection: {
146
+ tabList: {
147
+ attributes: {
148
+ 'aria-label': 'Onglets de résultats',
149
+ },
150
+ },
151
+ tabButton: {
152
+ attributes: {
153
+ 'aria-label': (data) => `${data.tab.label || data.tab.id}${typeof data.resultCount == 'number' ? `, ${data.resultCount} résultats` : ''}`,
154
+ },
155
+ },
156
+ },
145
157
  perPage: {
146
158
  label: {
147
159
  value: 'Par page',
@@ -9,7 +9,12 @@ const snap_profiler_1 = require("@athoscommerce/snap-profiler");
9
9
  const snap_logger_1 = require("@athoscommerce/snap-logger");
10
10
  const snap_tracker_1 = require("@athoscommerce/snap-tracker");
11
11
  exports.default = (config, services) => {
12
- const urlManager = (services?.urlManager || new snap_url_manager_1.UrlManager(new snap_url_manager_1.UrlTranslator(config.url), snap_url_manager_1.reactLinker)).detach();
12
+ //grab the url globals and pull them out of the translatorConfig
13
+ const { globals: urlGlobals, ...translatorConfig } = config.url || {};
14
+ let urlManager = (services?.urlManager || new snap_url_manager_1.UrlManager(new snap_url_manager_1.UrlTranslator(translatorConfig), snap_url_manager_1.reactLinker)).detach();
15
+ if (urlGlobals?.length) {
16
+ urlManager = urlManager.withGlobals(Object.fromEntries(urlGlobals.map(({ param, value }) => [param, [value]])));
17
+ }
13
18
  // set client mode
14
19
  if (config.mode && config.client) {
15
20
  config.client.config = config.client.config || {};
@@ -1 +1 @@
1
- {"version":3,"file":"TemplateSelect.d.ts","sourceRoot":"","sources":["../../../../../../../components/src/components/Atoms/TemplateSelect/TemplateSelect.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,EAAmD,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAsB,MAAM,mDAAmD,CAAC;AAC5G,OAAO,EAAE,WAAW,EAAE,MAAM,iDAAiD,CAAC;AAE9E,eAAO,MAAM,cAAc,gBAAyB,mBAAmB;;CA+BrE,CAAC;AACH,MAAM,WAAW,mBAAmB;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,WAAW,CAAC;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;CACd"}
1
+ {"version":3,"file":"TemplateSelect.d.ts","sourceRoot":"","sources":["../../../../../../../components/src/components/Atoms/TemplateSelect/TemplateSelect.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAmB,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAmD,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAsB,MAAM,mDAAmD,CAAC;AAC5G,OAAO,EAAE,WAAW,EAAE,MAAM,iDAAiD,CAAC;AAE9E,eAAO,MAAM,cAAc,gBAAyB,mBAAmB;;CAyCrE,CAAC;AACH,MAAM,WAAW,mBAAmB;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,WAAW,CAAC;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;CACd"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "@emotion/react/jsx-runtime";
2
2
  import { observer } from 'mobx-react-lite';
3
+ import { ControllerTypes } from '@athoscommerce/snap-controller';
3
4
  import { ThemeProvider, ControllerProvider, SnapProvider } from '../../../providers';
4
5
  export const TemplateSelect = observer((properties) => {
5
6
  const { snap, templatesStore, target, controller, ...otherProps } = properties;
@@ -16,6 +17,9 @@ export const TemplateSelect = observer((properties) => {
16
17
  const error = `Theme "${target.theme.name}" not found in library for target "${target.selector}"`;
17
18
  controller.log.error(error);
18
19
  }
20
+ const tabManager = controller.type === ControllerTypes.search || controller.type === ControllerTypes.autocomplete
21
+ ? templatesStore.getTabManager(controller.type, snap.controllers)
22
+ : undefined;
19
23
  // ensuring that theme and component are ready to render
20
- return !loading && theme && Component ? (_jsx(SnapProvider, { snap: snap, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ControllerProvider, { controller: controller, children: _jsx("div", { className: "ss__template-select", children: _jsx(Component, { controller: controller, ...otherProps }) }) }) }) })) : null;
24
+ return !loading && theme && Component ? (_jsx(SnapProvider, { snap: snap, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(ControllerProvider, { controller: controller, children: _jsx("div", { className: "ss__template-select", children: _jsx(Component, { controller: controller, tabManager: tabManager, name: tabManager?.active?.id ? tabManager?.active?.id.toLowerCase() : undefined, ...otherProps }) }) }) }) })) : null;
21
25
  });