@shopgate/engage 7.28.0-beta.6 → 7.29.0-alpha.2
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/category/components/CategoryImage/index.js +1 -1
- package/category/components/CategoryList/index.js +7 -3
- package/category/components/CategoryList/style.js +1 -1
- package/components/ResponsiveContainer/breakpoints.js +2 -2
- package/components/SheetList/components/Item/index.js +7 -7
- package/components/Typography/Typography.d.ts +132 -0
- package/components/Typography/Typography.js +11 -0
- package/components/Typography/index.js +1 -0
- package/components/View/components/Content/style.js +1 -1
- package/components/View/context.js +1 -1
- package/components/index.js +1 -1
- package/core/constants/index.js +7 -1
- package/core/contexts/ThemeResourcesContext.d.ts +10 -1
- package/core/contexts/ThemeResourcesContext.js +1 -1
- package/core/helpers/scrollContainer.js +2 -2
- package/core/hocs/withThemeResources.js +4 -1
- package/core/hooks/events/index.js +1 -1
- package/core/hooks/events/usePressHandler.js +38 -0
- package/core/hooks/useThemeResources.js +6 -5
- package/core/providers/ThemeResourcesProvider.js +9 -5
- package/locations/subscriptions.js +2 -2
- package/package.json +9 -7
- package/page/action-creators/index.js +22 -0
- package/page/components/Widgets/Overlay.js +51 -0
- package/page/components/Widgets/Tooltip.js +22 -0
- package/page/components/Widgets/Widget.js +17 -0
- package/page/components/Widgets/WidgetContext.d.ts +42 -0
- package/page/components/Widgets/WidgetContext.js +9 -0
- package/page/components/Widgets/WidgetProvider.js +8 -0
- package/page/components/Widgets/Widgets.js +11 -0
- package/page/components/Widgets/WidgetsPreviewContext.js +9 -0
- package/page/components/Widgets/WidgetsPreviewProvider.js +8 -0
- package/page/components/Widgets/constants.js +4 -0
- package/page/components/Widgets/events.js +23 -0
- package/page/components/Widgets/helpers.js +23 -0
- package/page/components/Widgets/hooks.js +69 -0
- package/page/components/Widgets/index.js +1 -0
- package/page/components/Widgets/types.d.ts +132 -0
- package/page/components/index.js +1 -1
- package/page/constants/actionTypes.js +1 -0
- package/page/constants/index.js +5 -1
- package/page/helpers/index.d.ts +47 -0
- package/page/helpers/index.js +12 -0
- package/page/hooks/index.d.ts +60 -0
- package/page/hooks/index.js +25 -0
- package/page/index.js +1 -2
- package/page/reducers/index.js +15 -0
- package/page/selectors/index.js +48 -2
- package/page/subscriptions/index.js +4 -0
- package/page/widgets/CategoryList/CategoryList.js +4 -0
- package/page/widgets/CategoryList/hooks.js +14 -0
- package/page/widgets/CategoryList/index.js +1 -0
- package/page/widgets/CategoryList/selectors.js +8 -0
- package/page/widgets/HTML/HTML.js +5 -0
- package/page/widgets/HTML/hooks.js +12 -0
- package/page/widgets/HTML/index.js +1 -0
- package/page/widgets/Headline/Headline.js +5 -0
- package/page/widgets/Headline/hooks.js +8 -0
- package/page/widgets/Headline/index.js +1 -0
- package/page/widgets/Placeholder/Placeholder.js +5 -0
- package/page/widgets/Placeholder/hooks.js +12 -0
- package/page/widgets/Placeholder/index.js +1 -0
- package/page/widgets/ProductList/ProductList.js +5 -0
- package/page/widgets/ProductList/hooks.js +19 -0
- package/page/widgets/ProductList/index.js +1 -0
- package/page/widgets/ProductSlider/ProductSlider.js +5 -0
- package/page/widgets/ProductSlider/hooks.js +22 -0
- package/page/widgets/ProductSlider/index.js +1 -0
- package/page/widgets/index.js +1 -0
- package/page/widgets/widgets.json +20 -0
- package/product/components/ProductCard/index.js +1 -1
- package/product/components/ProductGrid/components/Item/components/ItemDetails/index.js +8 -0
- package/product/components/ProductGrid/components/Item/components/ItemDetails/spec.js +1 -0
- package/product/components/ProductGrid/components/Item/components/ItemDiscount/index.js +5 -0
- package/product/components/ProductGrid/components/Item/components/ItemFavoritesButton/index.js +5 -0
- package/product/components/ProductGrid/components/Item/components/ItemFavoritesButton/spec.js +1 -0
- package/product/components/ProductGrid/components/Item/components/ItemImage/index.js +5 -0
- package/product/components/ProductGrid/components/Item/components/ItemImage/spec.js +1 -0
- package/product/components/ProductGrid/components/Item/components/ItemName/index.js +5 -0
- package/product/components/ProductGrid/components/Item/components/ItemName/spec.js +1 -0
- package/product/components/ProductGrid/components/Item/components/ItemPrice/index.js +5 -0
- package/product/components/ProductGrid/components/Item/components/ItemPrice/spec.js +1 -0
- package/product/components/ProductGrid/components/Item/index.js +7 -0
- package/product/components/ProductGrid/components/Iterator/index.js +5 -0
- package/product/components/ProductGrid/components/Layout/index.js +5 -0
- package/product/components/ProductGrid/index.js +22 -0
- package/product/components/ProductGrid/spec.js +1 -0
- package/product/components/ProductGridPrice/index.js +1 -1
- package/product/components/ProductSlider/index.js +4 -4
- package/product/components/index.js +1 -1
- package/styles/helpers/color.js +23 -0
- package/styles/helpers/index.js +1 -1
- package/styles/helpers/setPageBackgroundColor.js +2 -2
- package/styles/index.d.ts +17 -0
- package/styles/index.js +1 -1
- package/styles/theme/createTheme/createBreakpoints.d.ts +114 -0
- package/styles/theme/createTheme/createBreakpoints.js +41 -0
- package/styles/theme/createTheme/createPalette.d.ts +36 -0
- package/styles/theme/createTheme/createPalette.js +4 -0
- package/styles/theme/createTheme/createSpacing.d.ts +23 -0
- package/styles/theme/createTheme/createSpacing.js +14 -0
- package/styles/theme/createTheme/createTypography.d.ts +55 -0
- package/styles/theme/createTheme/createTypography.js +23 -0
- package/styles/theme/createTheme/index.d.ts +41 -0
- package/styles/theme/createTheme/index.js +5 -0
- package/styles/theme/createTheme/transitions.d.ts +100 -0
- package/styles/theme/createTheme/transitions.js +26 -0
- package/styles/theme/createTheme/zIndex.d.ts +12 -0
- package/styles/theme/createTheme/zIndex.js +3 -0
- package/styles/theme/hooks/index.d.ts +4 -0
- package/styles/theme/hooks/index.js +1 -0
- package/styles/theme/hooks/useActiveBreakpoint.d.ts +18 -0
- package/styles/theme/hooks/useActiveBreakpoint.js +4 -0
- package/styles/theme/hooks/useMediaQuery.d.ts +33 -0
- package/styles/theme/hooks/useMediaQuery.js +20 -0
- package/styles/theme/hooks/useResponsiveValue.d.ts +27 -0
- package/styles/theme/hooks/useResponsiveValue.js +4 -0
- package/styles/theme/hooks/useTheme.d.ts +8 -0
- package/styles/theme/hooks/useTheme.js +4 -0
- package/styles/theme/index.d.ts +8 -0
- package/styles/theme/index.js +1 -0
- package/styles/theme/providers/ActiveBreakpointProvider.d.ts +21 -0
- package/styles/theme/providers/ActiveBreakpointProvider.js +13 -0
- package/styles/theme/providers/ThemeProvider.d.ts +18 -0
- package/styles/theme/providers/ThemeProvider.js +7 -0
- package/styles/tss/index.js +3 -0
- package/tracking/selectors/cookieConsent.js +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function _slicedToArray(arr,i){return _arrayWithHoles(arr)||_iterableToArrayLimit(arr,i)||_nonIterableRest();}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance");}function _iterableToArrayLimit(arr,i){var _arr=[];var _n=true;var _d=false;var _e=undefined;try{for(var _i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break;}}catch(err){_d=true;_e=err;}finally{try{if(!_n&&_i["return"]!=null)_i["return"]();}finally{if(_d)throw _e;}}return _arr;}function _arrayWithHoles(arr){if(Array.isArray(arr))return arr;}function _extends(){_extends=Object.assign||function(target){for(var i=1;i<arguments.length;i++){var source=arguments[i];for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){target[key]=source[key];}}}return target;};return _extends.apply(this,arguments);}import React,{useState,useRef,useEffect,useCallback}from'react';import PropTypes from'prop-types';import{makeStyles,keyframes,colorToRgba}from'@shopgate/engage/styles';import{useRoute}from'@shopgate/engage/core/hooks';import{getScrollContainer}from"./helpers";import{useWidgetPreviewEvent}from"./events";import{useWidgetsPreview}from"./hooks";/**
|
|
2
|
+
* @typedef {Object} OverlayStyle
|
|
3
|
+
* @property {number} top Style for the top position of the overlay.
|
|
4
|
+
* @property {number} left Style for the left position of the overlay.
|
|
5
|
+
* @property {number} width Style for the width of the overlay.
|
|
6
|
+
* @property {number} height Style for the height of the overlay.
|
|
7
|
+
*/ /**
|
|
8
|
+
* @typedef {Object} MarginOverlayStyles
|
|
9
|
+
* @property {OverlayStyle} top Style for the top margin overlay.
|
|
10
|
+
* @property {OverlayStyle} left Style for the left margin overlay.
|
|
11
|
+
* @property {OverlayStyle} bottom Style for the bottom margin overlay.
|
|
12
|
+
* @property {OverlayStyle} right Style for the right margin overlay.
|
|
13
|
+
*/var useStyles=makeStyles({name:'WidgetPreviewOverlay'})(function(_,_ref){var highlightColor=_ref.highlightColor,overlayBorderColor=_ref.overlayBorderColor,marginOverlayColor=_ref.marginOverlayColor,isFlashing=_ref.isFlashing;return{root:{},mainOverlay:_extends({position:'absolute',pointerEvents:'none',zIndex:10,boxShadow:'0 0 8px 2px rgba(34, 42, 69, 0.07)',outline:"1px solid ".concat(overlayBorderColor||'#50A9AD')},isFlashing&&{animationName:keyframes({'0%':{backgroundColor:'transparent'},'50%':{backgroundColor:colorToRgba(highlightColor||'#50A9AD',0.5)},'100%':{backgroundColor:'transparent'}}),animationDuration:'0.5s',animationTimingFunction:'ease-in-out',animationFillMode:'forwards'}),marginOverlay:{position:'absolute',backgroundColor:colorToRgba(marginOverlayColor||'#50A9AD',0.1),pointerEvents:'none',zIndex:9}};});/**
|
|
14
|
+
* The Overlay component is used to highlight the active widget when preview mode is active.
|
|
15
|
+
* It also visualizes the margins of the widget and the borders to its sibling widgets.
|
|
16
|
+
* @param {Object} props The component props.
|
|
17
|
+
* @param {React.Ref<HTMLDivElement>} props.containerRef The reference to the container element that
|
|
18
|
+
* holds the widgets.
|
|
19
|
+
* @returns {JSX.Element|null}
|
|
20
|
+
*/var Overlay=function Overlay(_ref2){var containerRef=_ref2.containerRef;var _useRoute=useRoute(),_useRoute$query=_useRoute.query,highlightColor=_useRoute$query.highlightColor,overlayBorderColor=_useRoute$query.overlayBorderColor,marginOverlayColor=_useRoute$query.marginOverlayColor;var _useWidgetsPreview=useWidgetsPreview(),activeWidget=_useWidgetsPreview.activeWidget;/**
|
|
21
|
+
* State to hold the style for the main overlay that highlights the active widget.
|
|
22
|
+
* @type {[OverlayStyle|null, React.Dispatch<React.SetStateAction<OverlayStyle|null>>]}
|
|
23
|
+
*/var _useState=useState(null),_useState2=_slicedToArray(_useState,2),mainOverlayStyle=_useState2[0],setMainOverlayStyle=_useState2[1];/**
|
|
24
|
+
* State to hold the styles for the margin overlays that visualize the widget margins.
|
|
25
|
+
* @type {[MarginOverlayStyles|null,
|
|
26
|
+
* React.Dispatch<React.SetStateAction<MarginOverlayStyles|null>>]}
|
|
27
|
+
*/var _useState3=useState(null),_useState4=_slicedToArray(_useState3,2),marginOverlays=_useState4[0],setMarginOverlays=_useState4[1];var _useState5=useState(false),_useState6=_slicedToArray(_useState5,2),isFlashing=_useState6[0],setIsFlashing=_useState6[1];var _useStyles=useStyles({highlightColor:highlightColor,overlayBorderColor:overlayBorderColor,marginOverlayColor:marginOverlayColor,isFlashing:isFlashing}),classes=_useStyles.classes;/**
|
|
28
|
+
* @type {import('react').MutableRefObject<ResizeObserver|null>}
|
|
29
|
+
*/var resizeRef=useRef(null);/**
|
|
30
|
+
* @type {import('react').MutableRefObject<MutationObserver|null>}
|
|
31
|
+
*/var mutationRef=useRef(null);/**
|
|
32
|
+
* Callback to update the overlay position, margin overlays and size based on the active widget.
|
|
33
|
+
*/var updateOverlay=useCallback(function(){if(!containerRef.current||!activeWidget){return;}var target=containerRef.current.querySelector("#widget-code-".concat(activeWidget));if(!target){setMainOverlayStyle(null);return;}var scrollContainer=getScrollContainer();// Get the computed styles of the active widget to calculate margins
|
|
34
|
+
var styles=window.getComputedStyle(target);var marginLeft=parseFloat(styles.marginLeft);var marginRight=parseFloat(styles.marginRight);var marginTop=parseFloat(styles.marginTop);var marginBottom=parseFloat(styles.marginBottom);// Get bounding rectangles for the target widget and the scroll container
|
|
35
|
+
var elementRect=target.getBoundingClientRect();var containerRect=scrollContainer.getBoundingClientRect();var baseTop=elementRect.top-containerRect.top+scrollContainer.scrollTop;var baseLeft=elementRect.left-containerRect.left+scrollContainer.scrollLeft;var top=baseTop;var left=baseLeft-marginLeft;var width=target.offsetWidth+marginLeft+marginRight;var height=target.offsetHeight;// Keep a backdoor to re-enable overlay outline inside the widget margins
|
|
36
|
+
var mainOverlayBordersOnMarginEdges=true;var mainTop=baseTop-(mainOverlayBordersOnMarginEdges?marginTop:0);var mainHeight=height+(mainOverlayBordersOnMarginEdges?marginTop+marginBottom:0);setMainOverlayStyle({top:mainTop+1,left:left,width:width,height:mainHeight-2});setMarginOverlays({top:{top:top-marginTop,left:left,width:width,height:marginTop},bottom:{top:top+height,left:left,width:width,height:marginBottom},left:{top:top,left:left,width:marginLeft,height:height},right:{top:top,left:left+width-marginRight,width:marginRight,height:height}});},[activeWidget,containerRef]);// Effect to setup observers that watch for changes in the container and its children.
|
|
37
|
+
// Needed to update the overlay style when the layout changes.
|
|
38
|
+
useEffect(function(){var containerEl=containerRef.current;if(!containerEl)return undefined;// Create a ResizeObserver to watch for size changes of children
|
|
39
|
+
resizeRef.current=new ResizeObserver(function(){// Whenever any observed child resizes, update overlay
|
|
40
|
+
updateOverlay();});// Observe all existing children
|
|
41
|
+
Array.from(containerEl.children).forEach(function(child){if(child.nodeType===Node.ELEMENT_NODE){resizeRef.current.observe(child);}});// Create one MutationObserver on the container to watch for changes in the DOM
|
|
42
|
+
mutationRef.current=new MutationObserver(function(mutations){// eslint-disable-next-line no-restricted-syntax
|
|
43
|
+
var _iteratorNormalCompletion=true;var _didIteratorError=false;var _iteratorError=undefined;try{for(var _iterator=mutations[Symbol.iterator](),_step;!(_iteratorNormalCompletion=(_step=_iterator.next()).done);_iteratorNormalCompletion=true){var mutation=_step.value;if(mutation.type==='childList'){// Handle newly added nodes - observe them for size changes
|
|
44
|
+
mutation.addedNodes.forEach(function(node){if(node.nodeType===Node.ELEMENT_NODE){resizeRef.current.observe(node);}});// Handle removed nodes - remove them from observation
|
|
45
|
+
mutation.removedNodes.forEach(function(node){if(node.nodeType===Node.ELEMENT_NODE){resizeRef.current.unobserve(node);}});// If children were added/removed, recalculate overlay position
|
|
46
|
+
updateOverlay();}else if(mutation.type==='attributes'&&(mutation.attributeName==='class'||mutation.attributeName==='style')){// Update overlay if the class or style of a child changes
|
|
47
|
+
if(mutation.target.parentElement===containerEl){updateOverlay();}}}}catch(err){_didIteratorError=true;_iteratorError=err;}finally{try{if(!_iteratorNormalCompletion&&_iterator["return"]!=null){_iterator["return"]();}}finally{if(_didIteratorError){throw _iteratorError;}}}});// Start observing:
|
|
48
|
+
// - childList:true → to catch added/removed children
|
|
49
|
+
// - subtree:true + attributes:true → to catch any class/style changes in descendants
|
|
50
|
+
mutationRef.current.observe(containerEl,{childList:true,subtree:true,attributes:true,attributeFilter:['class','style']});// Cleanup on unmount or if updateOverlay changes:
|
|
51
|
+
return function(){if(resizeRef.current){resizeRef.current.disconnect();resizeRef.current=null;}if(mutationRef.current){mutationRef.current.disconnect();mutationRef.current=null;}};},[containerRef,updateOverlay]);useWidgetPreviewEvent('highlight-widget',function(){setIsFlashing(true);});var handleAnimationEnd=useCallback(function(){setIsFlashing(false);},[]);if(!mainOverlayStyle)return null;return React.createElement("div",{className:classes.root},React.createElement("div",{className:classes.mainOverlay,style:mainOverlayStyle,onAnimationEnd:handleAnimationEnd}),marginOverlays&&Object.entries(marginOverlays).map(function(_ref3){var _ref4=_slicedToArray(_ref3,2),key=_ref4[0],overlayStyle=_ref4[1];return React.createElement("div",{key:key,className:classes.marginOverlay,style:overlayStyle});}));};export default Overlay;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function _extends(){_extends=Object.assign||function(target){for(var i=1;i<arguments.length;i++){var source=arguments[i];for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){target[key]=source[key];}}}return target;};return _extends.apply(this,arguments);}function _slicedToArray(arr,i){return _arrayWithHoles(arr)||_iterableToArrayLimit(arr,i)||_nonIterableRest();}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance");}function _iterableToArrayLimit(arr,i){var _arr=[];var _n=true;var _d=false;var _e=undefined;try{for(var _i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break;}}catch(err){_d=true;_e=err;}finally{try{if(!_n&&_i["return"]!=null)_i["return"]();}finally{if(_d)throw _e;}}return _arr;}function _arrayWithHoles(arr){if(Array.isArray(arr))return arr;}import React,{useState,useRef,useEffect}from'react';import ReactDOM from'react-dom';import PropTypes from'prop-types';import{makeStyles}from'@shopgate/engage/styles';var useStyles=makeStyles()(function(){return{wrapper:{display:'inline-block',position:'relative',cursor:'help'},tooltipBox:{position:'absolute',padding:'6px 10px',backgroundColor:'rgba(0, 0, 0, 0.87)',color:'#fff',fontSize:'0.875rem',fontWeight:400,borderRadius:'4px',whiteSpace:'normal',// allow multiline
|
|
2
|
+
wordBreak:'break-word',maxWidth:'200px',zIndex:1000,pointerEvents:'none',opacity:0,transform:'scale(0.9)',transition:'opacity 0.1s ease-in-out, transform 0.1s ease-in-out','&[data-visible="true"]':{opacity:1,transform:'scale(1)'}},arrowTop:{'&::before':{content:"''",position:'absolute',bottom:'-6px',left:'var(--arrow-left, 50%)',transform:'translateX(-50%)',borderWidth:'6px 6px 0 6px',borderStyle:'solid',borderColor:'rgba(0, 0, 0, 0.87) transparent transparent transparent'}},arrowBottom:{'&::before':{content:"''",position:'absolute',top:'-6px',left:'var(--arrow-left, 50%)',transform:'translateX(-50%) rotate(180deg)',borderWidth:'6px 6px 0 6px',borderStyle:'solid',borderColor:'rgba(0, 0, 0, 0.87) transparent transparent transparent'}}};});/**
|
|
3
|
+
* AI generated Tooltip component to display additional information on hover.
|
|
4
|
+
* @param {Object} props The component props.
|
|
5
|
+
* @param {React.ReactNode} props.children The child elements to wrap.
|
|
6
|
+
* @param {string} props.text The tooltip text to display.
|
|
7
|
+
* @returns {JSX.Element}
|
|
8
|
+
*/function Tooltip(_ref){var children=_ref.children,text=_ref.text;var _useStyles=useStyles(),classes=_useStyles.classes,cx=_useStyles.cx;var wrapperRef=useRef(null);var _useState=useState(false),_useState2=_slicedToArray(_useState,2),visible=_useState2[0],setVisible=_useState2[1];var _useState3=useState(false),_useState4=_slicedToArray(_useState3,2),mounted=_useState4[0],setMounted=_useState4[1];var _useState5=useState(false),_useState6=_slicedToArray(_useState5,2),animate=_useState6[0],setAnimate=_useState6[1];var _useState7=useState({left:0,top:0}),_useState8=_slicedToArray(_useState7,2),coords=_useState8[0],setCoords=_useState8[1];var _useState9=useState(null),_useState10=_slicedToArray(_useState9,2),arrowLeft=_useState10[0],setArrowLeft=_useState10[1];var _useState11=useState('top'),_useState12=_slicedToArray(_useState11,2),positionState=_useState12[0],setPositionState=_useState12[1];// 'top' or 'bottom'
|
|
9
|
+
var tooltipId=useRef(Math.random().toString(36).slice(2,11));// Handle mounting/unmounting and trigger fade animation
|
|
10
|
+
useEffect(function(){var timeoutId;if(visible){setMounted(true);// allow DOM to insert before starting the fade-in
|
|
11
|
+
timeoutId=setTimeout(function(){return setAnimate(true);},10);}else{setAnimate(false);// after fade-out duration, unmount
|
|
12
|
+
timeoutId=setTimeout(function(){return setMounted(false);},200);}return function(){return clearTimeout(timeoutId);};},[visible]);// Compute position, flipping if needed, when mounted
|
|
13
|
+
useEffect(function(){if(!mounted||!wrapperRef.current)return;var wrapperRect=wrapperRef.current.getBoundingClientRect();var selector=".tooltip-box[data-tooltip-id=\"".concat(tooltipId.current,"\"]");var tooltipEl=document.querySelector(selector);if(!tooltipEl)return;var ttRect=tooltipEl.getBoundingClientRect();var margin=8;var vw=window.innerWidth;var vh=window.innerHeight;// 1) Try "top" placement
|
|
14
|
+
var left=wrapperRect.left+wrapperRect.width/2-ttRect.width/2;var top=wrapperRect.top-ttRect.height-8;// Clamp horizontal even before deciding flip, to calculate arrow offset
|
|
15
|
+
if(left<margin){left=margin;}else if(left+ttRect.width>vw-margin){left=vw-ttRect.width-margin;}// If top would be too high (tooltip clipped), switch to "bottom"
|
|
16
|
+
var finalPosition='top';if(top<margin){// try bottom
|
|
17
|
+
var bottomTop=wrapperRect.bottom+8;if(bottomTop+ttRect.height<=vh-margin){finalPosition='bottom';top=bottomTop;}else{// can't fit fully in bottom either; clamp top to margin
|
|
18
|
+
top=margin;}}// If using "top", ensure vertical clamp if it goes off bottom
|
|
19
|
+
if(finalPosition==='top'){if(top+ttRect.height>vh-margin){top=vh-ttRect.height-margin;}}// If using "bottom", clamp bottom if it would go off bottom
|
|
20
|
+
if(finalPosition==='bottom'){if(top+ttRect.height>vh-margin){top=vh-ttRect.height-margin;}}// 2) Compute arrow offset so it points to wrapper’s center X
|
|
21
|
+
var wrapperCenterX=wrapperRect.left+wrapperRect.width/2;var computedArrowLeft=wrapperCenterX-left;// Clamp arrow within [6px, ttRect.width - 6px]
|
|
22
|
+
var minArrow=6;var maxArrow=ttRect.width-6;if(computedArrowLeft<minArrow){computedArrowLeft=minArrow;}else if(computedArrowLeft>maxArrow){computedArrowLeft=maxArrow;}setPositionState(finalPosition);setCoords({left:left,top:top});setArrowLeft(computedArrowLeft);},[mounted]);if(!text){return children;}var portalStyle=_extends({left:"".concat(coords.left,"px"),top:"".concat(coords.top,"px")},arrowLeft!==null?{'--arrow-left':"".concat(arrowLeft,"px")}:{});var arrowClass=positionState==='top'?classes.arrowTop:classes.arrowBottom;return React.createElement("span",{className:classes.wrapper,ref:wrapperRef,"data-tooltip-id":tooltipId.current,onMouseEnter:function onMouseEnter(){return setVisible(true);},onMouseLeave:function onMouseLeave(){return setVisible(false);}},children,mounted&&ReactDOM.createPortal(React.createElement("div",{className:cx(classes.tooltipBox,arrowClass,'tooltip-box'),style:portalStyle,"data-visible":animate?'true':'false',"data-tooltip-id":tooltipId.current,dangerouslySetInnerHTML:{__html:text}}),document.body));}Tooltip.defaultProps={text:null};export default Tooltip;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function _extends(){_extends=Object.assign||function(target){for(var i=1;i<arguments.length;i++){var source=arguments[i];for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){target[key]=source[key];}}}return target;};return _extends.apply(this,arguments);}function _defineProperty(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true});}else{obj[key]=value;}return obj;}import React,{Suspense,useCallback}from'react';import PropTypes from'prop-types';import{makeStyles}from'@shopgate/engage/styles';import{VisibilityOffIcon,TimeIcon,Loading}from'@shopgate/engage/components';import{usePressHandler}from'@shopgate/engage/core/hooks';import WidgetProvider from"./WidgetProvider";import{dispatchWidgetPreviewEvent}from"./events";import{useWidgetsPreview}from"./hooks";import Tooltip from"./Tooltip";var useStyles=makeStyles()(function(theme,_ref){var marginTop=_ref.marginTop,marginLeft=_ref.marginLeft;return{root:{position:'relative'},widgetInfo:{zIndex:12,position:'absolute',top:-marginTop+(theme.spacing(0.5)+1),left:-marginLeft+theme.spacing(0.5),fontSize:24,padding:theme.spacing(0.5),display:'flex',gap:theme.spacing(1),background:'#fff',borderRadius:4,border:'1px solid rgba(0, 0, 0, 0.23)',':empty':{display:'none'}},preview:{cursor:'pointer'},visibilityIcon:{color:'#f44336'},scheduledIcon:{color:'#347DD3'},scheduledIconExpired:{color:'#f44336'}};});/**
|
|
2
|
+
* @typedef {import('./types').WidgetDefinition} WidgetDefinition
|
|
3
|
+
*/ /**
|
|
4
|
+
* @typedef {import('./types').ScheduledStatus} ScheduledStatus
|
|
5
|
+
*/ /**
|
|
6
|
+
* The Widget component.
|
|
7
|
+
* @param {Object} props The component props.
|
|
8
|
+
* @param {React.ComponentType} props.component The widget component to render.
|
|
9
|
+
* @param {WidgetDefinition} props.definition The widget definition data.
|
|
10
|
+
* @param {boolean} props.isPreview Whether the widget is in preview mode.
|
|
11
|
+
* @param {boolean} props.isCustomLegacyWidget Whether the widget is a legacy custom widget provided
|
|
12
|
+
* by an extension that's configured via an HTML comment inside a HTML widget.
|
|
13
|
+
* @returns {JSX.Element}
|
|
14
|
+
*/var Widget=function Widget(_ref2){var _ref3,_definition$layout,_ref4,_definition$layout2,_ref5,_definition$layout3,_ref6,_definition$layout4,_definition$layout5,_definition$layout6,_definition$layout7,_definition$layout8,_definition$meta,_definition$meta$sche,_definition$meta2,_definition$meta2$sch,_definition$meta3,_definition$meta3$sch,_definition$meta4,_definition$meta4$hid,_definition$meta5,_definition$meta5$hid;var Component=_ref2.component,definition=_ref2.definition,isPreview=_ref2.isPreview,isCustomLegacyWidget=_ref2.isCustomLegacyWidget;var _useStyles=useStyles({marginTop:(_ref3=definition===null||definition===void 0?void 0:(_definition$layout=definition.layout)===null||_definition$layout===void 0?void 0:_definition$layout.marginTop)!==null&&_ref3!==void 0?_ref3:0,marginBottom:(_ref4=definition===null||definition===void 0?void 0:(_definition$layout2=definition.layout)===null||_definition$layout2===void 0?void 0:_definition$layout2.marginBottom)!==null&&_ref4!==void 0?_ref4:0,marginLeft:(_ref5=definition===null||definition===void 0?void 0:(_definition$layout3=definition.layout)===null||_definition$layout3===void 0?void 0:_definition$layout3.marginLeft)!==null&&_ref5!==void 0?_ref5:0,marginRight:(_ref6=definition===null||definition===void 0?void 0:(_definition$layout4=definition.layout)===null||_definition$layout4===void 0?void 0:_definition$layout4.marginRight)!==null&&_ref6!==void 0?_ref6:0}),classes=_useStyles.classes,cx=_useStyles.cx;var _useWidgetsPreview=useWidgetsPreview(),setActiveWidget=_useWidgetsPreview.setActiveWidget,activeWidget=_useWidgetsPreview.activeWidget;// Handle clicks on the widget container in preview mode. Take care that highlighting only happens
|
|
15
|
+
// when the widget is not already active, otherwise it would be confusing when users want to
|
|
16
|
+
// interact with widget elements.
|
|
17
|
+
var handleInteraction=useCallback(function(){setActiveWidget(definition.code,activeWidget!==definition.code);if(activeWidget!==definition.code){dispatchWidgetPreviewEvent('widget-clicked',definition.code);}},[activeWidget,definition.code,setActiveWidget]);var handlers=usePressHandler(handleInteraction);if(!Component){return null;}return React.createElement("section",_extends({id:"widget-code-".concat(definition.code),className:cx(classes.root,_defineProperty({},classes.preview,isPreview)),style:{marginTop:definition===null||definition===void 0?void 0:(_definition$layout5=definition.layout)===null||_definition$layout5===void 0?void 0:_definition$layout5.marginTop,marginBottom:definition===null||definition===void 0?void 0:(_definition$layout6=definition.layout)===null||_definition$layout6===void 0?void 0:_definition$layout6.marginBottom,marginLeft:definition===null||definition===void 0?void 0:(_definition$layout7=definition.layout)===null||_definition$layout7===void 0?void 0:_definition$layout7.marginLeft,marginRight:definition===null||definition===void 0?void 0:(_definition$layout8=definition.layout)===null||_definition$layout8===void 0?void 0:_definition$layout8.marginRight},"data-widget-name":definition.widgetConfigDefinitionCode},isPreview&&_extends({},handlers)),isPreview&&(definition===null||definition===void 0?void 0:definition.meta)&&React.createElement("div",{className:classes.widgetInfo},((_definition$meta=definition.meta)===null||_definition$meta===void 0?void 0:(_definition$meta$sche=_definition$meta.scheduled)===null||_definition$meta$sche===void 0?void 0:_definition$meta$sche.isScheduled)&&React.createElement(Tooltip,{text:(_definition$meta2=definition.meta)===null||_definition$meta2===void 0?void 0:(_definition$meta2$sch=_definition$meta2.scheduled)===null||_definition$meta2$sch===void 0?void 0:_definition$meta2$sch.tooltip},React.createElement(TimeIcon,{className:cx(classes.scheduledIcon,_defineProperty({},classes.scheduledIconExpired,(_definition$meta3=definition.meta)===null||_definition$meta3===void 0?void 0:(_definition$meta3$sch=_definition$meta3.scheduled)===null||_definition$meta3$sch===void 0?void 0:_definition$meta3$sch.isExpired))})),((_definition$meta4=definition.meta)===null||_definition$meta4===void 0?void 0:(_definition$meta4$hid=_definition$meta4.hidden)===null||_definition$meta4$hid===void 0?void 0:_definition$meta4$hid.isHidden)&&React.createElement(Tooltip,{text:(_definition$meta5=definition.meta)===null||_definition$meta5===void 0?void 0:(_definition$meta5$hid=_definition$meta5.hidden)===null||_definition$meta5$hid===void 0?void 0:_definition$meta5$hid.tooltip},React.createElement(VisibilityOffIcon,{className:classes.visibilityIcon}))),React.createElement(WidgetProvider,{definition:definition,isPreview:isPreview},React.createElement(Suspense,{fallback:React.createElement(Loading,null)},React.createElement(Component,isCustomLegacyWidget?{settings:definition.widgetConfig}:{}))));};Widget.defaultProps={isCustomLegacyWidget:false};export default Widget;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Context } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type WidgetDefinitionLayout,
|
|
4
|
+
type WidgetDefinitionVisibility,
|
|
5
|
+
type WidgetDefinition,
|
|
6
|
+
} from './types'
|
|
7
|
+
|
|
8
|
+
export { WidgetDefinition } from './types';
|
|
9
|
+
|
|
10
|
+
export interface WidgetContextType<C = Record<string, any>> {
|
|
11
|
+
/**
|
|
12
|
+
* The unique code of the widget instance
|
|
13
|
+
*/
|
|
14
|
+
code: string;
|
|
15
|
+
/**
|
|
16
|
+
* The name of the widget
|
|
17
|
+
*/
|
|
18
|
+
name: string;
|
|
19
|
+
/**
|
|
20
|
+
* The widget configuration
|
|
21
|
+
*/
|
|
22
|
+
config: C;
|
|
23
|
+
/**
|
|
24
|
+
* The widget layout settings
|
|
25
|
+
*/
|
|
26
|
+
layout: WidgetDefinitionLayout;
|
|
27
|
+
/**
|
|
28
|
+
* The widget visibility settings
|
|
29
|
+
*/
|
|
30
|
+
visibility: WidgetDefinitionVisibility;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the widget is rendered in preview mode
|
|
33
|
+
*/
|
|
34
|
+
isPreview: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* React context for widgets.
|
|
39
|
+
*/
|
|
40
|
+
declare const WidgetContext: Context<WidgetContextType>;
|
|
41
|
+
|
|
42
|
+
export default WidgetContext;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{createContext}from'react';/**
|
|
2
|
+
* @typedef {import('./Widgets.jsx').WidgetDefinition} WidgetDefinition
|
|
3
|
+
*/ /**
|
|
4
|
+
* @typedef {Object} WidgetContextType
|
|
5
|
+
* @property {WidgetDefinition['code']} code The unique widget code.
|
|
6
|
+
* @property {WidgetDefinition['widgetConfig']} config The widget configuration.
|
|
7
|
+
* @property {WidgetDefinition['layout']} layout The widget layout settings.
|
|
8
|
+
* @property {WidgetDefinition['visibility']} visibility The widget visibility settings.
|
|
9
|
+
*/ /** @type {React.Context<WidgetContextType>} */export var WidgetContext=createContext({});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React,{useMemo}from'react';import PropTypes from'prop-types';import{WidgetContext}from"./WidgetContext";/** @typedef {import('./WidgetContext').WidgetContextType} WidgetContextType */ /** @typedef {import('./WidgetContext').WidgetDefinition} WidgetDefinition */ /**
|
|
2
|
+
* The WidgetProvider component provides the context for a single widget.
|
|
3
|
+
* @param {Object} props The component props.
|
|
4
|
+
* @param {WidgetDefinition} props.definition The widget definition data.
|
|
5
|
+
* @param {boolean} props.isPreview Whether the widget is in preview mode.
|
|
6
|
+
* @param {React.ReactNode} props.children The child components to render.
|
|
7
|
+
* @returns {JSX.Element}
|
|
8
|
+
*/var WidgetProvider=function WidgetProvider(_ref){var children=_ref.children,definition=_ref.definition,isPreview=_ref.isPreview;/** @type {WidgetContextType} */var value=useMemo(function(){var widgetConfig=definition.widgetConfig,layout=definition.layout,visibility=definition.visibility,code=definition.code,widgetConfigDefinitionCode=definition.widgetConfigDefinitionCode;return{code:code,name:widgetConfigDefinitionCode,config:widgetConfig,layout:layout,visibility:visibility,isPreview:isPreview};},[definition,isPreview]);return React.createElement(WidgetContext.Provider,{value:value},children);};export default WidgetProvider;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function _defineProperty(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true});}else{obj[key]=value;}return obj;}import React,{useMemo,useRef}from'react';import PropTypes from'prop-types';import{makeStyles}from'@shopgate/engage/styles';import{useRoute,useThemeWidgets}from'@shopgate/engage/core/hooks';import{PAGE_PREVIEW_PATTERN}from'@shopgate/engage/page/constants';import{ConditionalWrapper}from'@shopgate/engage/components';import WidgetsPreviewProvider from"./WidgetsPreviewProvider";import Widget from"./Widget";import Overlay from"./Overlay";import{usePreviewIframeCommunication}from"./hooks";/**
|
|
2
|
+
* @typedef {import('./types').WidgetDefinition} WidgetDefinition
|
|
3
|
+
*/var PLACEHOLDER_COMPONENT='@shopgate/widgetsInternal/Placeholder';var useStyles=makeStyles()({preview:{'& *':{scrollbarWidth:'thin'}}});/**
|
|
4
|
+
* The Widgets component renders a list of widgets.
|
|
5
|
+
* @param {Object} props The component props.
|
|
6
|
+
* @param {Array<WidgetDefinition>} props.widgets The list of widgets to render.
|
|
7
|
+
* @returns {JSX.Element}
|
|
8
|
+
*/var Widgets=function Widgets(_ref){var _ref$widgets=_ref.widgets,widgetsProp=_ref$widgets===void 0?[]:_ref$widgets;var _useStyles=useStyles(),classes=_useStyles.classes,cx=_useStyles.cx;var _useRoute=useRoute(),pattern=_useRoute.pattern;var widgetsRef=useRef(null);var isPreview=pattern===PAGE_PREVIEW_PATTERN;var widgetComponents=useThemeWidgets('v2');usePreviewIframeCommunication(isPreview);// Create sanitized widgets array that only includes widgets with valid components.
|
|
9
|
+
var widgets=useMemo(function(){if(isPreview){// All widgets are allowed in preview mode.
|
|
10
|
+
return widgetsProp;}// Remove widgets that do not have a valid component.
|
|
11
|
+
return widgetsProp.filter(function(widget){return!!widgetComponents[widget.widgetConfigDefinitionCode];});},[isPreview,widgetComponents,widgetsProp]);if(!Array.isArray(widgets)||widgets.length===0){return null;}return React.createElement(ConditionalWrapper,{condition:isPreview,wrapper:function wrapper(children){return React.createElement(WidgetsPreviewProvider,null,children,React.createElement(Overlay,{containerRef:widgetsRef}));}},React.createElement("div",{className:cx('engage__widgets',_defineProperty({},classes.preview,isPreview)),ref:widgetsRef},widgets.map(function(widget){var component=widgetComponents[widget.widgetConfigDefinitionCode]||widgetComponents[PLACEHOLDER_COMPONENT];return React.createElement(Widget,{key:widget.code,definition:widget,isPreview:isPreview,component:component,isCustomLegacyWidget:widget.isCustomLegacyWidget});})));};Widgets.defaultProps={widgets:null};export default Widgets;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import _noop from"lodash/noop";import{createContext}from'react';/**
|
|
2
|
+
* @callback SetActiveWidget
|
|
3
|
+
* @param {string} code The code of the widget to set as active.
|
|
4
|
+
* @param {boolean} [highlight=false] Whether to highlight the widget after setting it as active.
|
|
5
|
+
*/ /**
|
|
6
|
+
* @typedef {Object} WidgetsPreviewContextType
|
|
7
|
+
* @property {string} activeWidget The code of the currently active widget.
|
|
8
|
+
* @property {SetActiveWidget} setActiveWidget A function to set the active widget code
|
|
9
|
+
*/ /** @type {React.Context<WidgetsPreviewContextType>} */export var WidgetsPreviewContext=createContext({activeWidget:null,setActiveWidget:_noop});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
function _slicedToArray(arr,i){return _arrayWithHoles(arr)||_iterableToArrayLimit(arr,i)||_nonIterableRest();}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance");}function _iterableToArrayLimit(arr,i){var _arr=[];var _n=true;var _d=false;var _e=undefined;try{for(var _i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break;}}catch(err){_d=true;_e=err;}finally{try{if(!_n&&_i["return"]!=null)_i["return"]();}finally{if(_d)throw _e;}}return _arr;}function _arrayWithHoles(arr){if(Array.isArray(arr))return arr;}import React,{useMemo,useState,useCallback}from'react';import PropTypes from'prop-types';import{useWidgetPreviewEvent,dispatchWidgetPreviewEvent}from"./events";import{WidgetsPreviewContext}from"./WidgetsPreviewContext";/**
|
|
2
|
+
* The WidgetsPreviewProvider component is used by the Widgets component when it's rendered
|
|
3
|
+
* in preview mode. It provides functionality for the Widget component that's needed when
|
|
4
|
+
* the widgets are rendered in the preview iframe.
|
|
5
|
+
* @param {Object} props The component props.
|
|
6
|
+
* @param {React.ReactNode} props.children The child components to render.
|
|
7
|
+
* @returns {JSX.Element}
|
|
8
|
+
*/var WidgetsPreviewProvider=function WidgetsPreviewProvider(_ref){var children=_ref.children;var _useState=useState(null),_useState2=_slicedToArray(_useState,2),activeWidget=_useState2[0],setActiveWidget=_useState2[1];useWidgetPreviewEvent('set-active-widget-id',function(e){setActiveWidget(e.detail.widgetCode);});var handleSetActiveWidget=useCallback(function(code){var highlight=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;setActiveWidget(code);if(highlight){dispatchWidgetPreviewEvent('highlight-widget',code);}},[]);var value=useMemo(function(){return{activeWidget:activeWidget,setActiveWidget:handleSetActiveWidget};},[activeWidget,handleSetActiveWidget]);return React.createElement(WidgetsPreviewContext.Provider,{value:value},children);};export default WidgetsPreviewProvider;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List of allowed origins for cms page preview iFrame communication.
|
|
3
|
+
*/export var ALLOWED_PAGE_PREVIEW_ORIGINS=['https://next.admin.shopgatedev.com','https://next.admin.shopgatepg.com','https://next.admin.shopgate.com','https://next.us.admin.shopgate.com','http://localhost:1337'];// Whether to consider vertical margins when calculating the overlay position.
|
|
4
|
+
export var CONSIDER_CONTAINER_MARGINS_ON_SCROLL_DEFAULT=false;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import{useEffect}from'react';/**
|
|
2
|
+
* @typedef {"highlight-widget"|"widget-clicked"|"set-active-widget-id"} WidgetPreviewEventName
|
|
3
|
+
*/ /**
|
|
4
|
+
* @typedef {Object} WidgetPreviewEventDetail
|
|
5
|
+
* @property {string} widgetCode The code of the widget related to the event.
|
|
6
|
+
* @property {any} [payload] Optional payload data related to the event.
|
|
7
|
+
*/ /**
|
|
8
|
+
* @callback WidgetPreviewEventHandler
|
|
9
|
+
* @param {CustomEvent<WidgetPreviewEventDetail>} event The custom event dispatched for the widget
|
|
10
|
+
* preview.
|
|
11
|
+
*/ /**
|
|
12
|
+
* Hook to listen for widget preview events.
|
|
13
|
+
* These events are dispatched in the context of iFrame communication at the widget preview.
|
|
14
|
+
* @param {WidgetPreviewEventName} eventName Name of the listened event
|
|
15
|
+
* @param {WidgetPreviewEventHandler} handler A callback function to handle the event
|
|
16
|
+
*/export var useWidgetPreviewEvent=function useWidgetPreviewEvent(eventName,handler){useEffect(function(){window.addEventListener("widget-preview-".concat(eventName),handler);return function(){window.removeEventListener("widget-preview-".concat(eventName),handler);};},[eventName,handler]);};/**
|
|
17
|
+
* Dispatches widget preview related events.
|
|
18
|
+
* Used to trigger iFrame postMessage events to the parent window or to react on incoming
|
|
19
|
+
* postMessage events from the parent window.
|
|
20
|
+
* @param {WidgetPreviewEventName} eventName Name of the event to dispatch
|
|
21
|
+
* @param {string} widgetCode Code of the widget to dispatch the event for
|
|
22
|
+
* @param {Object} [payload] Optional payload to include with the event
|
|
23
|
+
*/export var dispatchWidgetPreviewEvent=function dispatchWidgetPreviewEvent(eventName,widgetCode){var payload=arguments.length>2&&arguments[2]!==undefined?arguments[2]:null;var event=new CustomEvent("widget-preview-".concat(eventName),{detail:{widgetCode:widgetCode,payload:payload}});window.dispatchEvent(event);};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import{PAGE_PREVIEW_PATTERN}from'@shopgate/engage/page/constants';/**
|
|
2
|
+
* Retrieves the scroll container for the current page. Depending on the PWA mode this can be
|
|
3
|
+
* a scrollable article element or the window.
|
|
4
|
+
* @returns {HTMLElement|null}
|
|
5
|
+
*/export var getScrollContainer=function getScrollContainer(){return document.querySelector(".route__".concat(PAGE_PREVIEW_PATTERN.replace(/^\/+/,'')));};/**
|
|
6
|
+
* @typedef {Object} ScheduledParams
|
|
7
|
+
* @param {string} [from] The start date of the scheduling in ISO format.
|
|
8
|
+
* @param {string} [to] The end date of the scheduling in ISO format.
|
|
9
|
+
* @param {number} [timezoneOffset] The timezone offset in minutes. If not provided, the local
|
|
10
|
+
* timezone offset will be used.
|
|
11
|
+
*/ /**
|
|
12
|
+
* @typedef {Object} ScheduledStatus
|
|
13
|
+
* @param {boolean} isScheduled Indicates if the widget is scheduled.
|
|
14
|
+
* @param {boolean} isActive Indicates if the widget is currently active within the
|
|
15
|
+
* scheduled time frame.
|
|
16
|
+
* @param {boolean} isExpired Indicates if the scheduled time frame has expired.
|
|
17
|
+
*/ /**
|
|
18
|
+
* Retrieves the scheduling status of a widget based on the provided parameters.
|
|
19
|
+
* @param {ScheduledParams} params The parameters for the function.
|
|
20
|
+
* @returns {ScheduledStatus} An object containing the scheduling status.
|
|
21
|
+
*/export function checkScheduled(){var _ref=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{},from=_ref.from,to=_ref.to,timezoneOffset=_ref.timezoneOffset;var now=new Date();// Convert current time to provided or local timezone
|
|
22
|
+
var localOffset=timezoneOffset!==null&&timezoneOffset!==void 0?timezoneOffset:-now.getTimezoneOffset();// in minutes
|
|
23
|
+
var offsetMs=localOffset*60*1000;var localNow=new Date(now.getTime()+offsetMs);var fromDate=from?new Date(from):null;var toDate=to?new Date(to):null;var isActive=(!fromDate||localNow>=new Date(fromDate.getTime()+offsetMs))&&(!toDate||localNow<=new Date(toDate.getTime()+offsetMs));var isExpired=!!toDate&&localNow>new Date(toDate.getTime()+offsetMs);var isScheduled=!!fromDate||!!toDate;return{isScheduled:isScheduled,isActive:isActive,isExpired:isExpired};}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import{useEffect,useCallback,useRef,useContext,useMemo}from'react';import{logger}from'@shopgate/engage/core/helpers';import{useDispatch}from'react-redux';import{useRoute}from'@shopgate/engage/core/hooks';import{receivePageConfigV2}from'@shopgate/engage/page/action-creators';import{PAGE_PREVIEW_SLUG}from'@shopgate/engage/page/constants';import{ALLOWED_PAGE_PREVIEW_ORIGINS,CONSIDER_CONTAINER_MARGINS_ON_SCROLL_DEFAULT}from"./constants";import{getScrollContainer}from"./helpers";import{WidgetsPreviewContext}from"./WidgetsPreviewContext";import{dispatchWidgetPreviewEvent,useWidgetPreviewEvent}from"./events";/**
|
|
2
|
+
* @typedef {Object} MessageData
|
|
3
|
+
* @property {string} type Identifier for the kind of message
|
|
4
|
+
* @property {any} [payload] Optional data payload for this message
|
|
5
|
+
*/ /**
|
|
6
|
+
* @typedef {Object} IframeMessengerResult
|
|
7
|
+
* @property {function(MessageData, string=): void} sendToParent
|
|
8
|
+
* - Send data up to window.parent. If targetOrigin is omitted, uses the
|
|
9
|
+
* most recently seen origin (from an incoming message). If none seen yet,
|
|
10
|
+
* falls back to parentOrigins[0] or "*".
|
|
11
|
+
*/ /**
|
|
12
|
+
* Hook for postMessage communication when your component is inside an iframe.
|
|
13
|
+
*
|
|
14
|
+
* Listens on window for "message" events from any origin in parentOrigins,
|
|
15
|
+
* and only calls onMessage(data, rawEvent) if both origin and source match.
|
|
16
|
+
*
|
|
17
|
+
* @param {function(MessageData, any): void} onMessage
|
|
18
|
+
* Callback invoked when a trusted message arrives. Receives data and the
|
|
19
|
+
* raw event (so you can inspect origin, source, etc.).
|
|
20
|
+
* @param {string[]} parentOrigins
|
|
21
|
+
* Array of allowed parent origin strings (e.g.
|
|
22
|
+
* ['https://a.example.com','https://b.example.com']).
|
|
23
|
+
* @returns {IframeMessengerResult}
|
|
24
|
+
* An object with a single method:
|
|
25
|
+
* • sendToParent(data, [targetOrigin]): void
|
|
26
|
+
* – Posts data up to window.parent. By default it uses the most recently
|
|
27
|
+
* seen origin (from an incoming message). If none, uses parentOrigins[0].
|
|
28
|
+
*/function useIframeMessenger(onMessage,parentOrigins){// Keep a ref to the latest onMessage callback so the listener always has it.
|
|
29
|
+
var onMessageRef=useRef(onMessage);useEffect(function(){onMessageRef.current=onMessage;},[onMessage]);// Keep track of the last allowed origin we heard from
|
|
30
|
+
var lastOriginRef=useRef(null);/**
|
|
31
|
+
* Send a message up to the parent window.
|
|
32
|
+
* @param {MessageData} data - The data object to post.
|
|
33
|
+
* @param {string} [targetOrigin]
|
|
34
|
+
* Optional override for the origin to post to. Must be one of
|
|
35
|
+
* parentOrigins. If omitted, uses the last seen origin (lastOriginRef),
|
|
36
|
+
* or parentOrigins[0], or "*" if array is empty.
|
|
37
|
+
*/var sendToParent=useCallback(function(data,targetOrigin){// Determine which origin to use: explicit, then last seen, then first, then "*".
|
|
38
|
+
var originToUse=typeof targetOrigin==='string'?targetOrigin:lastOriginRef.current||new URL(document.referrer).origin||parentOrigins[0]||'*';if(!originToUse){logger.warn('useIframeMessenger: no targetOrigin available. '+'Provide parentOrigins or pass targetOrigin.');return;}window.parent.postMessage(data,originToUse);},[parentOrigins]);// Attach / detach the "message" listener.
|
|
39
|
+
useEffect(function(){/**
|
|
40
|
+
* Handler for incoming postMessage events.
|
|
41
|
+
* @param {any} rawEvent – The original MessageEvent object.
|
|
42
|
+
*/function handler(rawEvent){// Only proceed if the origin is in our whitelist.
|
|
43
|
+
if(!parentOrigins.includes(rawEvent.origin))return;// Ensure the message actually came from window.parent.
|
|
44
|
+
if(rawEvent.source!==window.parent)return;// Record this origin as most recently seen.
|
|
45
|
+
lastOriginRef.current=rawEvent.origin;// Forward the event.data and the raw event to the callback.
|
|
46
|
+
onMessageRef.current(rawEvent.data,rawEvent);}window.addEventListener('message',handler);return function(){window.removeEventListener('message',handler);};},[parentOrigins,sendToParent]);return{sendToParent:sendToParent};}/**
|
|
47
|
+
* Hook to handle communication with the parent window in a page preview iframe.
|
|
48
|
+
* @param {boolean} isActive Whether the preview communication is active.
|
|
49
|
+
*/export var usePreviewIframeCommunication=function usePreviewIframeCommunication(){var isActive=arguments.length>0&&arguments[0]!==undefined?arguments[0]:false;var dispatch=useDispatch();var _useRoute=useRoute(),considerContainerMarginsOnScroll=_useRoute.query.considerContainerMarginsOnScroll;// Detect if container margins should be considered at scroll to widget.
|
|
50
|
+
var considerVerticalMargins=useMemo(function(){if(!considerContainerMarginsOnScroll){return CONSIDER_CONTAINER_MARGINS_ON_SCROLL_DEFAULT;}return considerContainerMarginsOnScroll==='true';},[considerContainerMarginsOnScroll]);var _useIframeMessenger=useIframeMessenger(function(data){var _data$payload;if(data.type==='receivePageConfig'){// Page preview config received from the parent window.
|
|
51
|
+
dispatch(receivePageConfigV2({type:'cms',slug:PAGE_PREVIEW_SLUG,data:data.payload}));}else if(data.type==='scrollToWidget'&&((_data$payload=data.payload)===null||_data$payload===void 0?void 0:_data$payload.widgetCode)){// Parent window requested to scroll to a specific widget.
|
|
52
|
+
var scrollContainer=getScrollContainer();var target=scrollContainer.querySelector("#widget-code-".concat(data.payload.widgetCode));if(scrollContainer&&target){var marginTop=0;if(considerVerticalMargins){var styles=window.getComputedStyle(target);marginTop=parseFloat(styles.marginTop);}var containerTop=scrollContainer.getBoundingClientRect().top;var targetTop=target.getBoundingClientRect().top;var scrollOffset=targetTop-containerTop+scrollContainer.scrollTop-marginTop;var maxScrollTop=scrollContainer.scrollHeight-scrollContainer.clientHeight;var actualScrollTop=Math.min(scrollOffset,maxScrollTop);// Register the target element as the active widget.
|
|
53
|
+
dispatchWidgetPreviewEvent('set-active-widget-id',data.payload.widgetCode);/**
|
|
54
|
+
* Callback to highlight the widget after scrolling.
|
|
55
|
+
*/var highlightWidget=function highlightWidget(){dispatchWidgetPreviewEvent('highlight-widget',data.payload.widgetCode);};// Add listener to onScrollEnd if available, otherwise use scroll event.
|
|
56
|
+
if('onscrollend'in scrollContainer){/**
|
|
57
|
+
* Callback for the scrollend event.
|
|
58
|
+
*/var onEnded=function onEnded(){scrollContainer.removeEventListener('scrollend',onEnded);highlightWidget();};scrollContainer.addEventListener('scrollend',onEnded,{once:true});scrollContainer.scrollTo({top:actualScrollTop,behavior:'smooth'});return;}// Fallback: listen for scroll events until scrollTop ≈ actualScrollTop
|
|
59
|
+
/**
|
|
60
|
+
* Callback for the scroll event.
|
|
61
|
+
*/var onScroll=function onScroll(){// Allow a 1 px leeway for subpixel rendering
|
|
62
|
+
if(Math.abs(scrollContainer.scrollTop-actualScrollTop)<1){scrollContainer.removeEventListener('scroll',onScroll);highlightWidget();}};scrollContainer.addEventListener('scroll',onScroll);scrollContainer.scrollTo({top:actualScrollTop,behavior:'smooth'});}}},ALLOWED_PAGE_PREVIEW_ORIGINS),sendToParent=_useIframeMessenger.sendToParent;useWidgetPreviewEvent('widget-clicked',function(e){if(!isActive){return;}sendToParent({type:'widgetClicked',payload:{widgetCode:e.detail.widgetCode}});});};/**
|
|
63
|
+
* @typedef {import('./WidgetsPreviewContext.js').WidgetsPreviewContextType}
|
|
64
|
+
* WidgetsPreviewContextType
|
|
65
|
+
*/ /**
|
|
66
|
+
* The useWidgetsPreview hook provides access to the context that is wrapped around the Widgets
|
|
67
|
+
* component when it's rendered in preview mode.
|
|
68
|
+
* @returns {WidgetsPreviewContextType} The widget context.
|
|
69
|
+
*/export var useWidgetsPreview=function useWidgetsPreview(){return useContext(WidgetsPreviewContext);};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{default}from"./Widgets";export{WidgetContext}from"./WidgetContext";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility settings for a widget.
|
|
3
|
+
*/
|
|
4
|
+
export interface WidgetDefinitionVisibility {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the widget is hidden.
|
|
7
|
+
*/
|
|
8
|
+
isHidden: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Start date for scheduled widgets.
|
|
11
|
+
*/
|
|
12
|
+
scheduleStartDate: string;
|
|
13
|
+
/**
|
|
14
|
+
* End date for scheduled widgets.
|
|
15
|
+
*/
|
|
16
|
+
scheduleEndDate: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Layout settings for a widget.
|
|
21
|
+
*/
|
|
22
|
+
export interface WidgetDefinitionLayout {
|
|
23
|
+
/**
|
|
24
|
+
* Top margin for the widget.
|
|
25
|
+
*/
|
|
26
|
+
marginTop: number;
|
|
27
|
+
/**
|
|
28
|
+
* Bottom margin for the widget.
|
|
29
|
+
*/
|
|
30
|
+
marginBottom: number;
|
|
31
|
+
/**
|
|
32
|
+
* Left margin for the widget.
|
|
33
|
+
*/
|
|
34
|
+
marginLeft: number;
|
|
35
|
+
/**
|
|
36
|
+
* Right margin for the widget.
|
|
37
|
+
*/
|
|
38
|
+
marginRight: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Definition of a API widget
|
|
43
|
+
*/
|
|
44
|
+
export interface WidgetDefinition {
|
|
45
|
+
/**
|
|
46
|
+
* Unique code for the widget.
|
|
47
|
+
*/
|
|
48
|
+
code: string;
|
|
49
|
+
/**
|
|
50
|
+
* Name of the widget
|
|
51
|
+
*/
|
|
52
|
+
widgetConfigDefinitionCode: string;
|
|
53
|
+
/**
|
|
54
|
+
* Individual configuration for the widget.
|
|
55
|
+
*/
|
|
56
|
+
widgetConfig: Record<string, any>;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the widget is a legacy custom widget provided by an extension that's configured
|
|
59
|
+
* via an HTML comment inside a HTML widget.
|
|
60
|
+
*/
|
|
61
|
+
isCustomLegacyWidget?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Visibility settings for the widget.
|
|
64
|
+
*/
|
|
65
|
+
visibility: WidgetDefinitionVisibility;
|
|
66
|
+
/**
|
|
67
|
+
* Layout settings for the widget.
|
|
68
|
+
*/
|
|
69
|
+
layout: WidgetDefinitionLayout;
|
|
70
|
+
/**
|
|
71
|
+
* Optional metadata for the widget (only available in preview mode)
|
|
72
|
+
*/
|
|
73
|
+
meta?: {
|
|
74
|
+
/**
|
|
75
|
+
* Hidden state related data
|
|
76
|
+
*/
|
|
77
|
+
hidden: {
|
|
78
|
+
/**
|
|
79
|
+
* Whether the widget is hidden.
|
|
80
|
+
*/
|
|
81
|
+
isHidden: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Tooltip text for hidden related UI elements.
|
|
84
|
+
*/
|
|
85
|
+
tooltip: string;
|
|
86
|
+
/**
|
|
87
|
+
* Label text for hidden related UI elements.
|
|
88
|
+
*/
|
|
89
|
+
label: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Scheduled state related data
|
|
93
|
+
*/
|
|
94
|
+
scheduled: {
|
|
95
|
+
/**
|
|
96
|
+
* Indicates if the widget is scheduled.
|
|
97
|
+
*/
|
|
98
|
+
isScheduled: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Indicates that the widget schedule time frame is currently active.
|
|
101
|
+
*/
|
|
102
|
+
isActive: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Indicates if the scheduled time frame has expired
|
|
105
|
+
*/
|
|
106
|
+
isExpired: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Tooltip text for schedule related UI elements.
|
|
109
|
+
*/
|
|
110
|
+
tooltip: string;
|
|
111
|
+
/**
|
|
112
|
+
* Label text for schedule related UI elements.
|
|
113
|
+
*/
|
|
114
|
+
label: string;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ScheduledStatus {
|
|
120
|
+
/**
|
|
121
|
+
* Indicates if the widget is scheduled.
|
|
122
|
+
*/
|
|
123
|
+
isScheduled: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Indicates if the widget is currently hidden based on the scheduling
|
|
126
|
+
*/
|
|
127
|
+
isHidden: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Indicates if the scheduled time frame has expired.
|
|
130
|
+
*/
|
|
131
|
+
isExpired: boolean;
|
|
132
|
+
}
|
package/page/components/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{default as NotFound}from"./NotFound";
|
|
1
|
+
export{default as NotFound}from"./NotFound";export{default as Widgets}from"./Widgets";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export var REQUEST_PAGE_CONFIG_V2='REQUEST_PAGE_CONFIG_V2';export var RECEIVE_PAGE_CONFIG_V2='RECEIVE_PAGE_CONFIG_V2';export var ERROR_PAGE_CONFIG_V2='ERROR_PAGE_CONFIG_V2';
|
package/page/constants/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
import{PAGE_PATH,PAGE_PATTERN}from'@shopgate/pwa-common/constants/RoutePaths';export*from'@shopgate/pwa-common/constants/PageIDs';export{PAGE_PATH,PAGE_PATTERN};export var IMPRINT_PATH="".concat(PAGE_PATH,"/imprint");export var PAYMENT_PATH="".concat(PAGE_PATH,"/payment");export var PRIVACY_PATH="".concat(PAGE_PATH,"/privacy");export var RETURN_POLICY_PATH="".concat(PAGE_PATH,"/return_policy");export var SHIPPING_PATH="".concat(PAGE_PATH,"/shipping");export var TERMS_PATH="".concat(PAGE_PATH,"/terms");
|
|
1
|
+
import{PAGE_PATH,PAGE_PATTERN}from'@shopgate/pwa-common/constants/RoutePaths';export*from'@shopgate/pwa-common/constants/PageIDs';export{PAGE_PATH,PAGE_PATTERN};export*from"./actionTypes";export var IMPRINT_PATH="".concat(PAGE_PATH,"/imprint");export var PAYMENT_PATH="".concat(PAGE_PATH,"/payment");export var PRIVACY_PATH="".concat(PAGE_PATH,"/privacy");export var RETURN_POLICY_PATH="".concat(PAGE_PATH,"/return_policy");export var SHIPPING_PATH="".concat(PAGE_PATH,"/shipping");export var TERMS_PATH="".concat(PAGE_PATH,"/terms");export var PAGE_PREVIEW_PATTERN='/shopgate-internal-page-preview';export var PAGE_PREVIEW_SLUG='page_preview';/**
|
|
2
|
+
* Checks if the app is currently in page preview mode.
|
|
3
|
+
*/export var IS_PAGE_PREVIEW_ACTIVE=window.location.pathname.startsWith(PAGE_PREVIEW_PATTERN);/**
|
|
4
|
+
* One hour in milliseconds
|
|
5
|
+
*/export var PAGE_STATE_LIFETIME=3600000;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type ProductsWidgetInputConfig = {
|
|
2
|
+
/**
|
|
3
|
+
* Source type for the product list
|
|
4
|
+
*/
|
|
5
|
+
productSelectorType: 'searchTerm' | 'brand' | 'category' | 'manualItemNumbers' | 'productSelector';
|
|
6
|
+
/**
|
|
7
|
+
* A search term to filter products by
|
|
8
|
+
*/
|
|
9
|
+
productsSearchTerm: string;
|
|
10
|
+
/**
|
|
11
|
+
* A brand to filter products by
|
|
12
|
+
*/
|
|
13
|
+
productsBrand: string;
|
|
14
|
+
/**
|
|
15
|
+
* A category to filter products by
|
|
16
|
+
*/
|
|
17
|
+
productsCategory: string;
|
|
18
|
+
/**
|
|
19
|
+
* Array of product item numbers (selected via manual input)
|
|
20
|
+
*/
|
|
21
|
+
productsManualItemNumbers: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Array of product item numbers (selected via product selector)
|
|
24
|
+
*/
|
|
25
|
+
productsSelectorItemNumbers: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type GetProductSearchParamsFromProductsInputConfigReturnValue = {
|
|
29
|
+
/**
|
|
30
|
+
* The type of product search to perform.
|
|
31
|
+
*/
|
|
32
|
+
productsSearchType: 'searchTerm' | 'brand' | 'category' | 'productIds';
|
|
33
|
+
/**
|
|
34
|
+
* The value to use for the product search. Can be a string or an array of strings (for product IDs).
|
|
35
|
+
*/
|
|
36
|
+
productsSearchValue: string | string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Helper to extract relevant search parameters from the widget configuration of the "Products"
|
|
41
|
+
* input.
|
|
42
|
+
*
|
|
43
|
+
* The return value can be used to e.g. parametrize the useWidgetProducts hook.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getProductSearchParamsFromProductsInputConfig(
|
|
46
|
+
products: ProductsWidgetInputConfig
|
|
47
|
+
): GetProductSearchParamsFromProductsInputConfigReturnValue;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* eslint-disable max-len */ /**
|
|
2
|
+
* @typedef {import('./').ProductsWidgetInputConfig} ProductsWidgetInputConfig
|
|
3
|
+
*/ /**
|
|
4
|
+
* @typedef {import('./').GetProductSearchParamsFromProductsInputConfigReturnValue} GetProductSearchParamsFromProductsInputConfigReturnValue
|
|
5
|
+
*/ /* eslint-enable max-len */ /**
|
|
6
|
+
* Helper to extract relevant search parameters from the widget configuration of the "Products"
|
|
7
|
+
* input.
|
|
8
|
+
* The return value can be used to e.g. parametrize the useWidgetProducts hook.
|
|
9
|
+
* @param {ProductsWidgetInputConfig} products Config object of the "Products" input.
|
|
10
|
+
* @returns {GetProductSearchParamsFromProductsInputConfigReturnValue}
|
|
11
|
+
*/export var getProductSearchParamsFromProductsInputConfig=function getProductSearchParamsFromProductsInputConfig(){var products=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{};var _ref=products||{},productSelectorType=_ref.productSelectorType,productsBrand=_ref.productsBrand,productsCategory=_ref.productsCategory,productsItemNumbers=_ref.productsItemNumbers,productsManualItemNumbers=_ref.productsManualItemNumbers,productsSelectorItemNumbers=_ref.productsSelectorItemNumbers,productsSearchTerm=_ref.productsSearchTerm;var productsSearchType=productSelectorType;/** @type {string|string[]} */var productsSearchValue='';switch(productSelectorType){case'brand':productsSearchValue=productsBrand;break;case'category':productsSearchValue=productsCategory;break;// Kept for backward compatibility - was replaces by 'manualItemNumbers' and 'productSelector'
|
|
12
|
+
case'itemNumbers':productsSearchValue=productsItemNumbers.split(',').map(function(item){return item.trim();});break;case'manualItemNumbers':productsSearchValue=productsManualItemNumbers;break;case'productSelector':productsSearchValue=productsSelectorItemNumbers;break;case'searchTerm':default:productsSearchValue=productsSearchTerm;}if(['itemNumbers','manualItemNumbers','productSelector'].includes(productSelectorType)){productsSearchType='productIds';}return{productsSearchType:productsSearchType,productsSearchValue:productsSearchValue};};
|