@constructor-io/constructorio-ui-plp 1.6.3 → 1.6.5

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.
@@ -16,25 +16,48 @@ function getValueFromOnChangeEvent(e) {
16
16
  }
17
17
  return parsedValue;
18
18
  }
19
- function FilterRangeSlider(props) {
19
+ /**
20
+ * Calculate the display range for the slider.
21
+ * When there's only one price value (min === max), use the user's previous selection
22
+ * to position the knobs at the edges. Otherwise, use the actual facet min/max.
23
+ */
24
+ function getDisplayRange(facet, isSingleValue) {
20
25
  var _a, _b;
26
+ if (isSingleValue && ((_a = facet.status) === null || _a === void 0 ? void 0 : _a.min) !== undefined && ((_b = facet.status) === null || _b === void 0 ? void 0 : _b.max) !== undefined) {
27
+ return {
28
+ displayMin: facet.status.min,
29
+ displayMax: facet.status.max,
30
+ };
31
+ }
32
+ return {
33
+ displayMin: facet.min,
34
+ displayMax: facet.max,
35
+ };
36
+ }
37
+ // Default track styles when range collapses to a single value
38
+ const COLLAPSED_TRACK_STYLE = { left: '0%', width: '100%' };
39
+ function FilterRangeSlider(props) {
40
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
21
41
  const { rangedFacet: facet, modifyRequestRangeFilter, isCollapsed, sliderStep = 0.1 } = props;
22
42
  const visibleTrack = (0, react_1.useRef)(null);
23
43
  const [selectedTrackStyles, setSelectedTrackStyles] = (0, react_1.useState)({});
24
- const [minValue, setMinValue] = (0, react_1.useState)(facet.min);
25
- const [maxValue, setMaxValue] = (0, react_1.useState)(facet.max);
26
- const [inputMinValue, setInputMinValue] = (0, react_1.useState)(((_a = facet.status) === null || _a === void 0 ? void 0 : _a.min) || facet.min);
27
- const [inputMaxValue, setInputMaxValue] = (0, react_1.useState)(((_b = facet.status) === null || _b === void 0 ? void 0 : _b.max) || facet.max);
28
- const [filterRange, setFilterRange] = (0, react_1.useState)('');
29
- const [isModified, setIsModified] = (0, react_1.useState)(false);
30
- const isValidMinValue = (value) => typeof value !== 'string' && value < maxValue && value >= facet.min;
31
- const isValidMaxValue = (value) => typeof value !== 'string' && value > minValue && value <= facet.max;
44
+ const isSingleValue = facet.min === facet.max;
45
+ const { displayMin, displayMax } = getDisplayRange(facet, isSingleValue);
46
+ const [minValue, setMinValue] = (0, react_1.useState)((_b = (_a = facet.status) === null || _a === void 0 ? void 0 : _a.min) !== null && _b !== void 0 ? _b : facet.min);
47
+ const [maxValue, setMaxValue] = (0, react_1.useState)((_d = (_c = facet.status) === null || _c === void 0 ? void 0 : _c.max) !== null && _d !== void 0 ? _d : facet.max);
48
+ const [inputMinValue, setInputMinValue] = (0, react_1.useState)((_f = (_e = facet.status) === null || _e === void 0 ? void 0 : _e.min) !== null && _f !== void 0 ? _f : facet.min);
49
+ const [inputMaxValue, setInputMaxValue] = (0, react_1.useState)((_h = (_g = facet.status) === null || _g === void 0 ? void 0 : _g.max) !== null && _h !== void 0 ? _h : facet.max);
50
+ const isValidMinValue = (value) => typeof value !== 'string' &&
51
+ value < maxValue &&
52
+ value >= (isSingleValue ? displayMin : facet.min);
53
+ const isValidMaxValue = (value) => typeof value !== 'string' &&
54
+ value > minValue &&
55
+ value <= (isSingleValue ? displayMax : facet.max);
32
56
  const onMinSliderMove = (event) => {
33
57
  const sliderValue = getValueFromOnChangeEvent(event);
34
58
  if (isValidMinValue(sliderValue)) {
35
59
  setMinValue(sliderValue);
36
60
  setInputMinValue(sliderValue);
37
- setIsModified(true);
38
61
  }
39
62
  };
40
63
  const onMaxSliderMove = (event) => {
@@ -42,7 +65,6 @@ function FilterRangeSlider(props) {
42
65
  if (isValidMaxValue(sliderValue)) {
43
66
  setMaxValue(sliderValue);
44
67
  setInputMaxValue(sliderValue);
45
- setIsModified(true);
46
68
  }
47
69
  };
48
70
  const onSliderMoveEnd = () => {
@@ -54,7 +76,6 @@ function FilterRangeSlider(props) {
54
76
  setInputMaxValue(inputValue);
55
77
  if (isValidMaxValue(inputValue)) {
56
78
  setMaxValue(inputValue);
57
- setIsModified(true);
58
79
  }
59
80
  };
60
81
  const onMinInputValueChange = (event) => {
@@ -62,12 +83,11 @@ function FilterRangeSlider(props) {
62
83
  setInputMinValue(inputValue);
63
84
  if (isValidMinValue(inputValue)) {
64
85
  setMinValue(inputValue);
65
- setIsModified(true);
66
86
  }
67
87
  };
68
88
  const onInputBlurApplyFilter = () => {
69
- if (isModified && isValidMaxValue(inputMaxValue) && isValidMinValue(inputMinValue)) {
70
- modifyRequestRangeFilter(filterRange);
89
+ if (isValidMaxValue(inputMaxValue) && isValidMinValue(inputMinValue)) {
90
+ modifyRequestRangeFilter(`${minValue}-${maxValue}`);
71
91
  }
72
92
  };
73
93
  const onTrackClick = (event) => {
@@ -75,9 +95,11 @@ function FilterRangeSlider(props) {
75
95
  return;
76
96
  if (event.target !== visibleTrack.current)
77
97
  return;
98
+ if (isSingleValue)
99
+ return; // Prevent interaction when only one price exists
78
100
  const totalWidth = visibleTrack.current.offsetWidth;
79
101
  const clickedX = event.nativeEvent.offsetX;
80
- const selectedValue = Math.round((clickedX / totalWidth) * (facet.max - facet.min)) + facet.min;
102
+ const selectedValue = Math.round((clickedX / totalWidth) * (displayMax - displayMin)) + displayMin;
81
103
  const distMinToClicked = Math.abs(selectedValue - minValue);
82
104
  const distMaxToClicked = Math.abs(selectedValue - maxValue);
83
105
  if (distMinToClicked <= distMaxToClicked) {
@@ -92,30 +114,40 @@ function FilterRangeSlider(props) {
92
114
  setInputMaxValue(selectedValue);
93
115
  modifyRequestRangeFilter(newRange);
94
116
  }
95
- setIsModified(true);
96
117
  };
97
- // Update internal state
118
+ // Update internal state when facet changes
98
119
  (0, react_1.useEffect)(() => {
99
- if (facet.status.min !== undefined && !isModified) {
100
- // Initial state
101
- setMinValue(facet.status.min);
102
- setMaxValue(facet.status.max);
103
- setFilterRange(`${facet.status.min}-${facet.status.max}`);
104
- }
105
- else if (isModified) {
106
- // Future updates
107
- setFilterRange(`${minValue}-${maxValue}`);
120
+ var _a;
121
+ if (((_a = facet.status) === null || _a === void 0 ? void 0 : _a.min) !== undefined) {
122
+ const clampedMin = Math.max(displayMin, Math.min(displayMax, facet.status.min));
123
+ const clampedMax = Math.max(displayMin, Math.min(displayMax, facet.status.max));
124
+ setMinValue(clampedMin);
125
+ setMaxValue(clampedMax);
126
+ setInputMinValue(isSingleValue ? facet.status.min : clampedMin);
127
+ setInputMaxValue(isSingleValue ? facet.status.max : clampedMax);
108
128
  }
109
- // eslint-disable-next-line react-hooks/exhaustive-deps
110
- }, [minValue, maxValue, facet]);
129
+ }, [
130
+ facet.min,
131
+ facet.max,
132
+ (_j = facet.status) === null || _j === void 0 ? void 0 : _j.min,
133
+ (_k = facet.status) === null || _k === void 0 ? void 0 : _k.max,
134
+ displayMin,
135
+ displayMax,
136
+ isSingleValue,
137
+ ]);
111
138
  // Update selected track styles
112
139
  (0, react_1.useEffect)(() => {
113
- const trackLen = facet.max - facet.min;
114
- const rebasedStartValue = minValue - facet.min;
140
+ const trackLen = displayMax - displayMin;
141
+ // Prevent division by zero when range collapses
142
+ if (trackLen === 0) {
143
+ setSelectedTrackStyles(COLLAPSED_TRACK_STYLE);
144
+ return;
145
+ }
146
+ const rebasedStartValue = minValue - displayMin;
115
147
  const startPercentage = ((100 * rebasedStartValue) / trackLen).toFixed(2);
116
148
  const widthPercentage = ((100 * (maxValue - minValue)) / trackLen).toFixed(2);
117
149
  setSelectedTrackStyles({ left: `${startPercentage}%`, width: `${widthPercentage}%` });
118
- }, [minValue, maxValue, facet]);
150
+ }, [minValue, maxValue, displayMin, displayMax]);
119
151
  return (react_1.default.createElement("div", { className: (0, classnames_1.default)({
120
152
  'cio-collapsible-wrapper': true,
121
153
  'cio-collapsible-is-open': !isCollapsed,
@@ -125,13 +157,13 @@ function FilterRangeSlider(props) {
125
157
  react_1.default.createElement("div", { className: 'cio-slider-inputs' },
126
158
  react_1.default.createElement("span", { className: 'cio-slider-input cio-slider-input-min' },
127
159
  react_1.default.createElement("span", { className: 'cio-slider-input-prefix' }, "from "),
128
- react_1.default.createElement("input", { required: true, type: 'number', value: inputMinValue, onChange: onMinInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.min.toString(), min: facet.min, max: maxValue, step: sliderStep })),
160
+ react_1.default.createElement("input", { required: true, type: 'number', value: inputMinValue, onChange: onMinInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.min.toString(), min: isSingleValue ? undefined : facet.min, max: isSingleValue ? undefined : maxValue, step: sliderStep, disabled: isSingleValue })),
129
161
  react_1.default.createElement("div", { className: 'cio-slider-input cio-slider-input-max' },
130
162
  react_1.default.createElement("span", { className: 'cio-slider-input-prefix' }, "to "),
131
- react_1.default.createElement("input", { required: true, type: 'number', value: inputMaxValue, onChange: onMaxInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.max.toString(), min: minValue, max: facet.max, step: sliderStep }))),
132
- react_1.default.createElement("div", { className: 'cio-doubly-ended-slider', ref: visibleTrack, role: 'presentation', onClick: (e) => onTrackClick(e) },
163
+ react_1.default.createElement("input", { required: true, type: 'number', value: inputMaxValue, onChange: onMaxInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.max.toString(), min: isSingleValue ? undefined : minValue, max: isSingleValue ? undefined : facet.max, step: sliderStep, disabled: isSingleValue }))),
164
+ react_1.default.createElement("div", { className: 'cio-doubly-ended-slider', ref: visibleTrack, role: 'presentation', onClick: (e) => !isSingleValue && onTrackClick(e) },
133
165
  react_1.default.createElement("div", { className: 'cio-slider-track-selected', style: selectedTrackStyles }),
134
- react_1.default.createElement("input", { className: 'cio-min-slider', type: 'range', step: sliderStep, min: facet.min, max: facet.max, value: minValue, onChange: onMinSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd }),
135
- react_1.default.createElement("input", { className: 'cio-max-slider', type: 'range', step: sliderStep, min: facet.min, max: facet.max, value: maxValue, onChange: onMaxSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd }))))));
166
+ react_1.default.createElement("input", { className: 'cio-min-slider', type: 'range', step: sliderStep, min: displayMin, max: displayMax, value: minValue, onChange: onMinSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd, disabled: isSingleValue }),
167
+ react_1.default.createElement("input", { className: 'cio-max-slider', type: 'range', step: sliderStep, min: displayMin, max: displayMax, value: maxValue, onChange: onMaxSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd, disabled: isSingleValue }))))));
136
168
  }
137
169
  exports.default = FilterRangeSlider;
@@ -30,7 +30,10 @@ function ProductCard(props) {
30
30
  const onAddToCart = (0, callbacks_1.useOnAddToCart)(client, state.callbacks.onAddToCart);
31
31
  const { formatPrice } = state.formatters;
32
32
  const onClick = (0, callbacks_1.useOnProductCardClick)(client, state.callbacks.onProductCardClick);
33
- const cnstrcData = (0, utils_1.getProductCardCnstrcDataAttributes)(productInfo);
33
+ const cnstrcDataAttrs = (0, utils_1.getProductCardCnstrcDataAttributes)(productInfo, {
34
+ labels: item.labels,
35
+ });
36
+ const addToCartBtnAttrs = (0, utils_1.getConversionButtonCnstrcDataAttributes)('add_to_cart');
34
37
  const handleRolloverImageState = (isShown) => {
35
38
  var _a;
36
39
  setIsRolloverImageShown(isShown);
@@ -63,9 +66,9 @@ function ProductCard(props) {
63
66
  onMouseEnter,
64
67
  onMouseLeave,
65
68
  isRolloverImageShown,
66
- productCardCnstrcDataAttributes: cnstrcData,
67
- }, override: children, htmlOverride: (_a = state.renderOverrides.productCard) === null || _a === void 0 ? void 0 : _a.renderHtml, topLevelAttributes: Object.assign(Object.assign({}, cnstrcData), { className: 'cio-product-card' }) },
68
- react_1.default.createElement("a", Object.assign({}, cnstrcData, { className: 'cio-product-card', href: itemUrl, ref: cardRef, onClick: (e) => { var _a; return onClick(e, item, (_a = productSwatch === null || productSwatch === void 0 ? void 0 : productSwatch.selectedVariation) === null || _a === void 0 ? void 0 : _a.variationId); } }),
69
+ productCardCnstrcDataAttributes: cnstrcDataAttrs,
70
+ }, override: children, htmlOverride: (_a = state.renderOverrides.productCard) === null || _a === void 0 ? void 0 : _a.renderHtml, topLevelAttributes: Object.assign(Object.assign({}, cnstrcDataAttrs), { className: 'cio-product-card' }) },
71
+ react_1.default.createElement("a", Object.assign({}, cnstrcDataAttrs, { className: 'cio-product-card', href: itemUrl, ref: cardRef, onClick: (e) => { var _a; return onClick(e, item, (_a = productSwatch === null || productSwatch === void 0 ? void 0 : productSwatch.selectedVariation) === null || _a === void 0 ? void 0 : _a.variationId); } }),
69
72
  react_1.default.createElement("div", { className: 'cio-image-container', onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave },
70
73
  react_1.default.createElement("img", { alt: itemName, src: itemImageUrl, className: 'cio-image' }),
71
74
  rolloverImage && (react_1.default.createElement("img", { alt: `${itemName} rollover`, src: rolloverImage, loading: 'lazy', className: (0, utils_1.concatStyles)('cio-image cio-rollover-image', isRolloverImageShown && 'is-active') }))),
@@ -75,6 +78,6 @@ function ProductCard(props) {
75
78
  Number(itemPrice) >= 0 && (react_1.default.createElement("div", { className: (0, utils_1.concatStyles)('cio-item-price', hasSalePrice && 'cio-item-price-strikethrough') }, formatPrice(itemPrice)))),
76
79
  react_1.default.createElement("div", { className: 'cio-item-name' }, itemName),
77
80
  productSwatch && react_1.default.createElement(ProductSwatch_1.default, { swatchObject: productSwatch })),
78
- react_1.default.createElement("button", { className: 'cio-add-to-cart-button', type: 'button', onClick: (e) => { var _a; return onAddToCart(e, item, itemPrice, (_a = productSwatch === null || productSwatch === void 0 ? void 0 : productSwatch.selectedVariation) === null || _a === void 0 ? void 0 : _a.variationId); } }, "Add to Cart"))));
81
+ react_1.default.createElement("button", Object.assign({}, addToCartBtnAttrs, { className: 'cio-add-to-cart-button', type: 'button', onClick: (e) => { var _a; return onAddToCart(e, item, itemPrice, (_a = productSwatch === null || productSwatch === void 0 ? void 0 : productSwatch.selectedVariation) === null || _a === void 0 ? void 0 : _a.variationId); } }), "Add to Cart"))));
79
82
  }
80
83
  exports.default = ProductCard;
@@ -34,9 +34,12 @@ function ProductSwatch(props) {
34
34
  const color = (0, utils_1.isHexColor)(swatch === null || swatch === void 0 ? void 0 : swatch.swatchPreview)
35
35
  ? swatch === null || swatch === void 0 ? void 0 : swatch.swatchPreview
36
36
  : `url(${swatch === null || swatch === void 0 ? void 0 : swatch.swatchPreview})`;
37
- return (react_1.default.createElement("button", { type: 'button', key: swatch.variationId, "data-cnstrc-variation-id": swatch.variationId, "data-testid": `cio-swatch-${swatch.variationId}`, className: 'cio-swatch-button cio-swatch-item', onClick: (e) => swatchClickHandler(e, swatch), style: {
37
+ const variationIdAttr = {
38
+ [utils_1.cnstrcDataAttrs.common.variationId]: swatch.variationId,
39
+ };
40
+ return (react_1.default.createElement("button", Object.assign({ type: 'button', key: swatch.variationId, "data-testid": `cio-swatch-${swatch.variationId}`, className: 'cio-swatch-button cio-swatch-item', onClick: (e) => swatchClickHandler(e, swatch), style: {
38
41
  background: color,
39
- } }, isSelected && (react_1.default.createElement("div", { "data-cnstrc-variation-id": swatch.variationId, className: 'cio-swatch-selected', style: { outline: `3px solid ${color}` } }))));
42
+ } }, variationIdAttr), isSelected && (react_1.default.createElement("div", Object.assign({ className: 'cio-swatch-selected', style: { outline: `3px solid ${color}` } }, variationIdAttr)))));
40
43
  }))))));
41
44
  }
42
45
  exports.default = ProductSwatch;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EMITTED_EVENTS = exports.DEMO_API_KEY = void 0;
4
- // eslint-disable-next-line import/prefer-default-export
5
4
  exports.DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x';
6
5
  exports.EMITTED_EVENTS = {
7
6
  PRODUCT_CARD_IMAGE_ROLLOVER: 'cio.ui-plp.productCardImageRollover',
@@ -69,14 +69,15 @@ function useCioPlp(props = {}) {
69
69
  const pagination = (0, usePagination_1.default)(Object.assign({ totalNumResults }, paginationConfigs));
70
70
  const groups = (0, useGroups_1.default)(Object.assign({ groups: rawGroups }, groupsConfigs));
71
71
  const data = isSearchPage ? search.data : browse.data;
72
- const plpContainerCnstrcDataAttributes = (0, utils_1.getPlpContainerCnstrcDataAttributes)(data, requestConfigs);
72
+ const status = isSearchPage ? search.status : browse.status;
73
+ const plpContainerCnstrcDataAttributes = (0, utils_1.getPlpContainerCnstrcDataAttributes)(data, requestConfigs, status === 'fetching');
73
74
  return {
74
75
  isSearchPage,
75
76
  isBrowsePage,
76
77
  searchQuery: search.query,
77
78
  browseFilterName: browse.filterName,
78
79
  browseFilterValue: browse.filterValue,
79
- status: isSearchPage ? search.status : browse.status,
80
+ status,
80
81
  data,
81
82
  refetch,
82
83
  filters,
@@ -1,42 +1,85 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getPlpContainerCnstrcDataAttributes = exports.getProductCardCnstrcDataAttributes = void 0;
3
+ exports.getConversionButtonCnstrcDataAttributes = exports.getPlpContainerCnstrcDataAttributes = exports.getProductCardCnstrcDataAttributes = exports.cnstrcDataAttrs = void 0;
4
4
  const typeHelpers_1 = require("./typeHelpers");
5
5
  const requestConfigsHelpers_1 = require("./requestConfigsHelpers");
6
- function getProductCardCnstrcDataAttributes(productInfo) {
7
- let dataCnstrc = {};
6
+ exports.cnstrcDataAttrs = {
7
+ common: {
8
+ itemId: 'data-cnstrc-item-id',
9
+ itemName: 'data-cnstrc-item-name',
10
+ itemPrice: 'data-cnstrc-item-price',
11
+ variationId: 'data-cnstrc-item-variation-id',
12
+ numResults: 'data-cnstrc-num-results',
13
+ conversionBtn: 'data-cnstrc-btn',
14
+ resultId: 'data-cnstrc-result-id',
15
+ section: 'data-cnstrc-section',
16
+ zeroResults: 'data-cnstrc-zero-result',
17
+ slCampaignId: 'data-cnstrc-sl-campaign-id',
18
+ slCampaignOwner: 'data-cnstrc-sl-campaign-owner',
19
+ },
20
+ search: {
21
+ searchContainer: 'data-cnstrc-search',
22
+ searchTerm: 'data-cnstrc-search-term',
23
+ },
24
+ browse: {
25
+ browseContainer: 'data-cnstrc-browse',
26
+ filterName: 'data-cnstrc-filter-name',
27
+ filterValue: 'data-cnstrc-filter-value',
28
+ },
29
+ };
30
+ function getProductCardCnstrcDataAttributes(productInfo, options) {
31
+ var _a, _b;
8
32
  const { itemId, itemPrice, itemName, variationId } = productInfo;
9
- dataCnstrc = {
10
- 'data-cnstrc-item-id': itemId,
11
- 'data-cnstrc-item-name': itemName,
12
- 'data-cnstrc-item-price': itemPrice,
13
- 'data-cnstrc-item-variation-id': variationId,
33
+ const dataCnstrc = {
34
+ [exports.cnstrcDataAttrs.common.itemId]: itemId,
35
+ [exports.cnstrcDataAttrs.common.itemName]: itemName,
14
36
  };
37
+ // Only include variation ID if it exists
38
+ if (variationId) {
39
+ dataCnstrc[exports.cnstrcDataAttrs.common.variationId] = variationId;
40
+ }
41
+ // Only include price if it exists
42
+ if (itemPrice !== undefined && itemPrice !== null) {
43
+ dataCnstrc[exports.cnstrcDataAttrs.common.itemPrice] = itemPrice;
44
+ }
45
+ // Add sponsored listing data if available
46
+ if ((_a = options === null || options === void 0 ? void 0 : options.labels) === null || _a === void 0 ? void 0 : _a.sl_campaign_id) {
47
+ dataCnstrc[exports.cnstrcDataAttrs.common.slCampaignId] = String(options.labels.sl_campaign_id);
48
+ }
49
+ if ((_b = options === null || options === void 0 ? void 0 : options.labels) === null || _b === void 0 ? void 0 : _b.sl_campaign_owner) {
50
+ dataCnstrc[exports.cnstrcDataAttrs.common.slCampaignOwner] = String(options.labels.sl_campaign_owner);
51
+ }
15
52
  return dataCnstrc;
16
53
  }
17
54
  exports.getProductCardCnstrcDataAttributes = getProductCardCnstrcDataAttributes;
18
- function getPlpContainerCnstrcDataAttributes(data, requestConfigs) {
55
+ function getPlpContainerCnstrcDataAttributes(data, requestConfigs, isLoading = false) {
56
+ var _a, _b;
19
57
  if (!data || (!(0, typeHelpers_1.isPlpSearchDataResults)(data) && !(0, typeHelpers_1.isPlpBrowseDataResults)(data)))
20
58
  return {};
21
59
  const { filterName, filterValue } = requestConfigs;
22
60
  const pageType = (0, requestConfigsHelpers_1.getPageType)(requestConfigs);
61
+ const isZeroResults = data.response.totalNumResults === 0;
23
62
  let dataCnstrc = {};
24
63
  switch (pageType) {
25
64
  case 'browse':
26
65
  dataCnstrc = {
27
- 'data-cnstrc-browse': true,
28
- 'data-cnstrc-num-results': data.response.totalNumResults,
29
- 'data-cnstrc-result-id': data.resultId,
30
- 'data-cnstrc-filter-name': filterName,
31
- 'data-cnstrc-filter-value': filterValue,
66
+ [exports.cnstrcDataAttrs.browse.browseContainer]: true,
67
+ [exports.cnstrcDataAttrs.common.numResults]: data.response.totalNumResults,
68
+ [exports.cnstrcDataAttrs.common.resultId]: data.resultId,
69
+ [exports.cnstrcDataAttrs.browse.filterName]: filterName,
70
+ [exports.cnstrcDataAttrs.browse.filterValue]: filterValue,
32
71
  };
33
72
  break;
34
73
  case 'search':
35
74
  dataCnstrc = {
36
- 'data-cnstrc-search': true,
37
- 'data-cnstrc-result-id': data.resultId,
38
- 'data-cnstrc-num-results': data.response.totalNumResults,
75
+ [exports.cnstrcDataAttrs.search.searchContainer]: true,
76
+ [exports.cnstrcDataAttrs.common.resultId]: data.resultId,
77
+ [exports.cnstrcDataAttrs.common.numResults]: data.response.totalNumResults,
39
78
  };
79
+ // Add search term
80
+ if ((_a = data.request) === null || _a === void 0 ? void 0 : _a.term) {
81
+ dataCnstrc[exports.cnstrcDataAttrs.search.searchTerm] = data.request.term;
82
+ }
40
83
  break;
41
84
  case 'unknown':
42
85
  dataCnstrc = {};
@@ -44,6 +87,23 @@ function getPlpContainerCnstrcDataAttributes(data, requestConfigs) {
44
87
  default:
45
88
  break;
46
89
  }
90
+ // Add common conditional attributes (applies to both search and browse)
91
+ if (pageType === 'search' || pageType === 'browse') {
92
+ // Add zero-result attribute only if not loading and there are no results
93
+ if (isZeroResults && !isLoading) {
94
+ dataCnstrc[exports.cnstrcDataAttrs.common.zeroResults] = true;
95
+ }
96
+ // Add section if it's not "Products"
97
+ if (((_b = data.request) === null || _b === void 0 ? void 0 : _b.section) && data.request.section !== 'Products') {
98
+ dataCnstrc[exports.cnstrcDataAttrs.common.section] = data.request.section;
99
+ }
100
+ }
47
101
  return dataCnstrc;
48
102
  }
49
103
  exports.getPlpContainerCnstrcDataAttributes = getPlpContainerCnstrcDataAttributes;
104
+ function getConversionButtonCnstrcDataAttributes(conversionType) {
105
+ return {
106
+ [exports.cnstrcDataAttrs.common.conversionBtn]: conversionType,
107
+ };
108
+ }
109
+ exports.getConversionButtonCnstrcDataAttributes = getConversionButtonCnstrcDataAttributes;
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.6.3';
3
+ exports.default = '1.6.5';
@@ -12,24 +12,46 @@ function getValueFromOnChangeEvent(e) {
12
12
  }
13
13
  return parsedValue;
14
14
  }
15
+ /**
16
+ * Calculate the display range for the slider.
17
+ * When there's only one price value (min === max), use the user's previous selection
18
+ * to position the knobs at the edges. Otherwise, use the actual facet min/max.
19
+ */
20
+ function getDisplayRange(facet, isSingleValue) {
21
+ if (isSingleValue && facet.status?.min !== undefined && facet.status?.max !== undefined) {
22
+ return {
23
+ displayMin: facet.status.min,
24
+ displayMax: facet.status.max,
25
+ };
26
+ }
27
+ return {
28
+ displayMin: facet.min,
29
+ displayMax: facet.max,
30
+ };
31
+ }
32
+ // Default track styles when range collapses to a single value
33
+ const COLLAPSED_TRACK_STYLE = { left: '0%', width: '100%' };
15
34
  export default function FilterRangeSlider(props) {
16
35
  const { rangedFacet: facet, modifyRequestRangeFilter, isCollapsed, sliderStep = 0.1 } = props;
17
36
  const visibleTrack = useRef(null);
18
37
  const [selectedTrackStyles, setSelectedTrackStyles] = useState({});
19
- const [minValue, setMinValue] = useState(facet.min);
20
- const [maxValue, setMaxValue] = useState(facet.max);
21
- const [inputMinValue, setInputMinValue] = useState(facet.status?.min || facet.min);
22
- const [inputMaxValue, setInputMaxValue] = useState(facet.status?.max || facet.max);
23
- const [filterRange, setFilterRange] = useState('');
24
- const [isModified, setIsModified] = useState(false);
25
- const isValidMinValue = (value) => typeof value !== 'string' && value < maxValue && value >= facet.min;
26
- const isValidMaxValue = (value) => typeof value !== 'string' && value > minValue && value <= facet.max;
38
+ const isSingleValue = facet.min === facet.max;
39
+ const { displayMin, displayMax } = getDisplayRange(facet, isSingleValue);
40
+ const [minValue, setMinValue] = useState(facet.status?.min ?? facet.min);
41
+ const [maxValue, setMaxValue] = useState(facet.status?.max ?? facet.max);
42
+ const [inputMinValue, setInputMinValue] = useState(facet.status?.min ?? facet.min);
43
+ const [inputMaxValue, setInputMaxValue] = useState(facet.status?.max ?? facet.max);
44
+ const isValidMinValue = (value) => typeof value !== 'string' &&
45
+ value < maxValue &&
46
+ value >= (isSingleValue ? displayMin : facet.min);
47
+ const isValidMaxValue = (value) => typeof value !== 'string' &&
48
+ value > minValue &&
49
+ value <= (isSingleValue ? displayMax : facet.max);
27
50
  const onMinSliderMove = (event) => {
28
51
  const sliderValue = getValueFromOnChangeEvent(event);
29
52
  if (isValidMinValue(sliderValue)) {
30
53
  setMinValue(sliderValue);
31
54
  setInputMinValue(sliderValue);
32
- setIsModified(true);
33
55
  }
34
56
  };
35
57
  const onMaxSliderMove = (event) => {
@@ -37,7 +59,6 @@ export default function FilterRangeSlider(props) {
37
59
  if (isValidMaxValue(sliderValue)) {
38
60
  setMaxValue(sliderValue);
39
61
  setInputMaxValue(sliderValue);
40
- setIsModified(true);
41
62
  }
42
63
  };
43
64
  const onSliderMoveEnd = () => {
@@ -49,7 +70,6 @@ export default function FilterRangeSlider(props) {
49
70
  setInputMaxValue(inputValue);
50
71
  if (isValidMaxValue(inputValue)) {
51
72
  setMaxValue(inputValue);
52
- setIsModified(true);
53
73
  }
54
74
  };
55
75
  const onMinInputValueChange = (event) => {
@@ -57,12 +77,11 @@ export default function FilterRangeSlider(props) {
57
77
  setInputMinValue(inputValue);
58
78
  if (isValidMinValue(inputValue)) {
59
79
  setMinValue(inputValue);
60
- setIsModified(true);
61
80
  }
62
81
  };
63
82
  const onInputBlurApplyFilter = () => {
64
- if (isModified && isValidMaxValue(inputMaxValue) && isValidMinValue(inputMinValue)) {
65
- modifyRequestRangeFilter(filterRange);
83
+ if (isValidMaxValue(inputMaxValue) && isValidMinValue(inputMinValue)) {
84
+ modifyRequestRangeFilter(`${minValue}-${maxValue}`);
66
85
  }
67
86
  };
68
87
  const onTrackClick = (event) => {
@@ -70,9 +89,11 @@ export default function FilterRangeSlider(props) {
70
89
  return;
71
90
  if (event.target !== visibleTrack.current)
72
91
  return;
92
+ if (isSingleValue)
93
+ return; // Prevent interaction when only one price exists
73
94
  const totalWidth = visibleTrack.current.offsetWidth;
74
95
  const clickedX = event.nativeEvent.offsetX;
75
- const selectedValue = Math.round((clickedX / totalWidth) * (facet.max - facet.min)) + facet.min;
96
+ const selectedValue = Math.round((clickedX / totalWidth) * (displayMax - displayMin)) + displayMin;
76
97
  const distMinToClicked = Math.abs(selectedValue - minValue);
77
98
  const distMaxToClicked = Math.abs(selectedValue - maxValue);
78
99
  if (distMinToClicked <= distMaxToClicked) {
@@ -87,30 +108,39 @@ export default function FilterRangeSlider(props) {
87
108
  setInputMaxValue(selectedValue);
88
109
  modifyRequestRangeFilter(newRange);
89
110
  }
90
- setIsModified(true);
91
111
  };
92
- // Update internal state
112
+ // Update internal state when facet changes
93
113
  useEffect(() => {
94
- if (facet.status.min !== undefined && !isModified) {
95
- // Initial state
96
- setMinValue(facet.status.min);
97
- setMaxValue(facet.status.max);
98
- setFilterRange(`${facet.status.min}-${facet.status.max}`);
99
- }
100
- else if (isModified) {
101
- // Future updates
102
- setFilterRange(`${minValue}-${maxValue}`);
114
+ if (facet.status?.min !== undefined) {
115
+ const clampedMin = Math.max(displayMin, Math.min(displayMax, facet.status.min));
116
+ const clampedMax = Math.max(displayMin, Math.min(displayMax, facet.status.max));
117
+ setMinValue(clampedMin);
118
+ setMaxValue(clampedMax);
119
+ setInputMinValue(isSingleValue ? facet.status.min : clampedMin);
120
+ setInputMaxValue(isSingleValue ? facet.status.max : clampedMax);
103
121
  }
104
- // eslint-disable-next-line react-hooks/exhaustive-deps
105
- }, [minValue, maxValue, facet]);
122
+ }, [
123
+ facet.min,
124
+ facet.max,
125
+ facet.status?.min,
126
+ facet.status?.max,
127
+ displayMin,
128
+ displayMax,
129
+ isSingleValue,
130
+ ]);
106
131
  // Update selected track styles
107
132
  useEffect(() => {
108
- const trackLen = facet.max - facet.min;
109
- const rebasedStartValue = minValue - facet.min;
133
+ const trackLen = displayMax - displayMin;
134
+ // Prevent division by zero when range collapses
135
+ if (trackLen === 0) {
136
+ setSelectedTrackStyles(COLLAPSED_TRACK_STYLE);
137
+ return;
138
+ }
139
+ const rebasedStartValue = minValue - displayMin;
110
140
  const startPercentage = ((100 * rebasedStartValue) / trackLen).toFixed(2);
111
141
  const widthPercentage = ((100 * (maxValue - minValue)) / trackLen).toFixed(2);
112
142
  setSelectedTrackStyles({ left: `${startPercentage}%`, width: `${widthPercentage}%` });
113
- }, [minValue, maxValue, facet]);
143
+ }, [minValue, maxValue, displayMin, displayMax]);
114
144
  return (React.createElement("div", { className: classNames({
115
145
  'cio-collapsible-wrapper': true,
116
146
  'cio-collapsible-is-open': !isCollapsed,
@@ -120,12 +150,12 @@ export default function FilterRangeSlider(props) {
120
150
  React.createElement("div", { className: 'cio-slider-inputs' },
121
151
  React.createElement("span", { className: 'cio-slider-input cio-slider-input-min' },
122
152
  React.createElement("span", { className: 'cio-slider-input-prefix' }, "from "),
123
- React.createElement("input", { required: true, type: 'number', value: inputMinValue, onChange: onMinInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.min.toString(), min: facet.min, max: maxValue, step: sliderStep })),
153
+ React.createElement("input", { required: true, type: 'number', value: inputMinValue, onChange: onMinInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.min.toString(), min: isSingleValue ? undefined : facet.min, max: isSingleValue ? undefined : maxValue, step: sliderStep, disabled: isSingleValue })),
124
154
  React.createElement("div", { className: 'cio-slider-input cio-slider-input-max' },
125
155
  React.createElement("span", { className: 'cio-slider-input-prefix' }, "to "),
126
- React.createElement("input", { required: true, type: 'number', value: inputMaxValue, onChange: onMaxInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.max.toString(), min: minValue, max: facet.max, step: sliderStep }))),
127
- React.createElement("div", { className: 'cio-doubly-ended-slider', ref: visibleTrack, role: 'presentation', onClick: (e) => onTrackClick(e) },
156
+ React.createElement("input", { required: true, type: 'number', value: inputMaxValue, onChange: onMaxInputValueChange, onBlur: onInputBlurApplyFilter, placeholder: facet.max.toString(), min: isSingleValue ? undefined : minValue, max: isSingleValue ? undefined : facet.max, step: sliderStep, disabled: isSingleValue }))),
157
+ React.createElement("div", { className: 'cio-doubly-ended-slider', ref: visibleTrack, role: 'presentation', onClick: (e) => !isSingleValue && onTrackClick(e) },
128
158
  React.createElement("div", { className: 'cio-slider-track-selected', style: selectedTrackStyles }),
129
- React.createElement("input", { className: 'cio-min-slider', type: 'range', step: sliderStep, min: facet.min, max: facet.max, value: minValue, onChange: onMinSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd }),
130
- React.createElement("input", { className: 'cio-max-slider', type: 'range', step: sliderStep, min: facet.min, max: facet.max, value: maxValue, onChange: onMaxSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd }))))));
159
+ React.createElement("input", { className: 'cio-min-slider', type: 'range', step: sliderStep, min: displayMin, max: displayMax, value: minValue, onChange: onMinSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd, disabled: isSingleValue }),
160
+ React.createElement("input", { className: 'cio-max-slider', type: 'range', step: sliderStep, min: displayMin, max: displayMax, value: maxValue, onChange: onMaxSliderMove, onMouseUp: onSliderMoveEnd, onTouchEnd: onSliderMoveEnd, disabled: isSingleValue }))))));
131
161
  }
@@ -3,7 +3,7 @@ import { useCioPlpContext } from '../../hooks/useCioPlpContext';
3
3
  import { useOnAddToCart, useOnProductCardClick } from '../../hooks/callbacks';
4
4
  import ProductSwatch from '../ProductSwatch';
5
5
  import useProductInfo from '../../hooks/useProduct';
6
- import { concatStyles, getProductCardCnstrcDataAttributes } from '../../utils';
6
+ import { concatStyles, getProductCardCnstrcDataAttributes, getConversionButtonCnstrcDataAttributes, } from '../../utils';
7
7
  import RenderPropsWrapper from '../RenderPropsWrapper/RenderPropsWrapper';
8
8
  import { EMITTED_EVENTS } from '../../constants';
9
9
  /**
@@ -26,7 +26,10 @@ export default function ProductCard(props) {
26
26
  const onAddToCart = useOnAddToCart(client, state.callbacks.onAddToCart);
27
27
  const { formatPrice } = state.formatters;
28
28
  const onClick = useOnProductCardClick(client, state.callbacks.onProductCardClick);
29
- const cnstrcData = getProductCardCnstrcDataAttributes(productInfo);
29
+ const cnstrcDataAttrs = getProductCardCnstrcDataAttributes(productInfo, {
30
+ labels: item.labels,
31
+ });
32
+ const addToCartBtnAttrs = getConversionButtonCnstrcDataAttributes('add_to_cart');
30
33
  const handleRolloverImageState = (isShown) => {
31
34
  setIsRolloverImageShown(isShown);
32
35
  if (isShown && rolloverImage) {
@@ -58,9 +61,9 @@ export default function ProductCard(props) {
58
61
  onMouseEnter,
59
62
  onMouseLeave,
60
63
  isRolloverImageShown,
61
- productCardCnstrcDataAttributes: cnstrcData,
62
- }, override: children, htmlOverride: state.renderOverrides.productCard?.renderHtml, topLevelAttributes: { ...cnstrcData, className: 'cio-product-card' } },
63
- React.createElement("a", { ...cnstrcData, className: 'cio-product-card', href: itemUrl, ref: cardRef, onClick: (e) => onClick(e, item, productSwatch?.selectedVariation?.variationId) },
64
+ productCardCnstrcDataAttributes: cnstrcDataAttrs,
65
+ }, override: children, htmlOverride: state.renderOverrides.productCard?.renderHtml, topLevelAttributes: { ...cnstrcDataAttrs, className: 'cio-product-card' } },
66
+ React.createElement("a", { ...cnstrcDataAttrs, className: 'cio-product-card', href: itemUrl, ref: cardRef, onClick: (e) => onClick(e, item, productSwatch?.selectedVariation?.variationId) },
64
67
  React.createElement("div", { className: 'cio-image-container', onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave },
65
68
  React.createElement("img", { alt: itemName, src: itemImageUrl, className: 'cio-image' }),
66
69
  rolloverImage && (React.createElement("img", { alt: `${itemName} rollover`, src: rolloverImage, loading: 'lazy', className: concatStyles('cio-image cio-rollover-image', isRolloverImageShown && 'is-active') }))),
@@ -70,5 +73,5 @@ export default function ProductCard(props) {
70
73
  Number(itemPrice) >= 0 && (React.createElement("div", { className: concatStyles('cio-item-price', hasSalePrice && 'cio-item-price-strikethrough') }, formatPrice(itemPrice)))),
71
74
  React.createElement("div", { className: 'cio-item-name' }, itemName),
72
75
  productSwatch && React.createElement(ProductSwatch, { swatchObject: productSwatch })),
73
- React.createElement("button", { className: 'cio-add-to-cart-button', type: 'button', onClick: (e) => onAddToCart(e, item, itemPrice, productSwatch?.selectedVariation?.variationId) }, "Add to Cart"))));
76
+ React.createElement("button", { ...addToCartBtnAttrs, className: 'cio-add-to-cart-button', type: 'button', onClick: (e) => onAddToCart(e, item, itemPrice, productSwatch?.selectedVariation?.variationId) }, "Add to Cart"))));
74
77
  }