@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.
- package/dist/constructorio-ui-plp-bundled.js +13 -13
- package/lib/cjs/components/Filters/FilterRangeSlider.js +69 -37
- package/lib/cjs/components/ProductCard/ProductCard.js +8 -5
- package/lib/cjs/components/ProductSwatch/ProductSwatch.js +5 -2
- package/lib/cjs/constants.js +0 -1
- package/lib/cjs/hooks/useCioPlp.js +3 -2
- package/lib/cjs/utils/dataAttributeHelpers.js +77 -17
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/Filters/FilterRangeSlider.js +66 -36
- package/lib/mjs/components/ProductCard/ProductCard.js +9 -6
- package/lib/mjs/components/ProductSwatch/ProductSwatch.js +6 -3
- package/lib/mjs/constants.js +0 -1
- package/lib/mjs/hooks/useCioPlp.js +3 -2
- package/lib/mjs/utils/dataAttributeHelpers.js +73 -16
- package/lib/mjs/version.js +1 -1
- package/lib/types/hooks/useCioPlp.d.ts +1 -1
- package/lib/types/types.d.ts +2 -2
- package/lib/types/utils/dataAttributeHelpers.d.ts +37 -3
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -16,25 +16,48 @@ function getValueFromOnChangeEvent(e) {
|
|
|
16
16
|
}
|
|
17
17
|
return parsedValue;
|
|
18
18
|
}
|
|
19
|
-
|
|
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
|
|
25
|
-
const
|
|
26
|
-
const [
|
|
27
|
-
const [
|
|
28
|
-
const [
|
|
29
|
-
const [
|
|
30
|
-
const isValidMinValue = (value) => typeof value !== 'string' &&
|
|
31
|
-
|
|
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 (
|
|
70
|
-
modifyRequestRangeFilter(
|
|
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) * (
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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 =
|
|
114
|
-
|
|
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,
|
|
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:
|
|
135
|
-
react_1.default.createElement("input", { className: 'cio-max-slider', type: 'range', step: sliderStep, min:
|
|
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
|
|
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:
|
|
67
|
-
}, override: children, htmlOverride: (_a = state.renderOverrides.productCard) === null || _a === void 0 ? void 0 : _a.renderHtml, topLevelAttributes: Object.assign(Object.assign({},
|
|
68
|
-
react_1.default.createElement("a", Object.assign({},
|
|
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
|
-
|
|
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", {
|
|
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;
|
package/lib/cjs/constants.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
package/lib/cjs/version.js
CHANGED
|
@@ -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
|
|
20
|
-
const
|
|
21
|
-
const [
|
|
22
|
-
const [
|
|
23
|
-
const [
|
|
24
|
-
const [
|
|
25
|
-
const isValidMinValue = (value) => typeof value !== 'string' &&
|
|
26
|
-
|
|
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 (
|
|
65
|
-
modifyRequestRangeFilter(
|
|
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) * (
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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 =
|
|
109
|
-
|
|
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,
|
|
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:
|
|
130
|
-
React.createElement("input", { className: 'cio-max-slider', type: 'range', step: sliderStep, min:
|
|
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
|
|
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:
|
|
62
|
-
}, override: children, htmlOverride: state.renderOverrides.productCard?.renderHtml, topLevelAttributes: { ...
|
|
63
|
-
React.createElement("a", { ...
|
|
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
|
}
|