UNPKG

@shopgate/engage

Version:
75 lines 9.72 kB
import{logger,isDev}from'@shopgate/engage/core/helpers';import{DATA_IGNORED}from"./constants";/** * Determines if an element is visually present in the DOM. * Checks for display, visibility, opacity, and transform-based hiding. * * @param {HTMLElement} el The element to evaluate. * @returns {boolean} True if the element is visually visible; false otherwise. */export var isElementVisible=function isElementVisible(el){if(!el||!(el instanceof HTMLElement))return false;var style=window.getComputedStyle(el);if(style.display==='none'||style.visibility==='hidden'||parseFloat(style.opacity)===0)return false;var transform=style.transform;if(transform&&transform!=='none'){var match=transform.match(/translateY\((-?\d+)(px)?\)/);if(match){var translateY=parseInt(match[1],10);if(translateY>window.innerHeight)return false;}}return true;};/** * Finds the widest nested element inside a given container that has a background color * explicitly set via CSS (ignores transparent, inherited, or unset values), and returns * that background color. * * @param {HTMLElement} container The parent element to search within. Must be an actual DOM node. * @returns {string|null} The detected background color (e.g., "rgb(255, 0, 0)") */export var getElementBackgroundColor=function getElementBackgroundColor(container){if(!container){return null;}var widestElement=null;var maxWidth=-Infinity;/** * Recursively traverses the DOM tree starting from the given node, * tracking the widest element that has a background color explicitly set via CSS. * * @param {HTMLElement} node The DOM node to begin traversal from. */function walk(node){if(!(node instanceof HTMLElement))return;var style=window.getComputedStyle(node);var bgColor=style.backgroundColor;var isStyledColor=bgColor&&!['transparent','rgba(0, 0, 0, 0)','inherit','initial','unset'].includes(bgColor);var rect=node.getBoundingClientRect();if(isElementVisible(node)&&isStyledColor&&rect.width>maxWidth){maxWidth=rect.width;widestElement=node;}Array.from(node.children).forEach(walk);}walk(container);if(widestElement){var result=window.getComputedStyle(widestElement).backgroundColor;return result;}return null;};/** * Checks if any of the provided class names reference the custom property in any loaded stylesheet. * * @param {string[]} classList Array of class names to check. * @param {string} customProp The custom property to search for in the stylesheets. * @returns {boolean} True if any class rule uses the custom property. */var classNamesUseCustomProp=function classNamesUseCustomProp(classList,customProp){var allRules=Array.from(document.styleSheets).filter(function(sheet){try{return sheet.cssRules;}catch(e){return false;// Skip cross-origin or restricted stylesheets }}).flatMap(function(sheet){return Array.from(sheet.cssRules||[]);});// eslint-disable-next-line no-restricted-syntax var _iteratorNormalCompletion=true;var _didIteratorError=false;var _iteratorError=undefined;try{for(var _iterator=allRules[Symbol.iterator](),_step;!(_iteratorNormalCompletion=(_step=_iterator.next()).done);_iteratorNormalCompletion=true){var rule=_step.value;// eslint-disable-next-line no-continue if(!rule.selectorText||!rule.cssText.includes("var(".concat(customProp,")")))continue;// eslint-disable-next-line no-restricted-syntax var _iteratorNormalCompletion2=true;var _didIteratorError2=false;var _iteratorError2=undefined;try{for(var _iterator2=classList[Symbol.iterator](),_step2;!(_iteratorNormalCompletion2=(_step2=_iterator2.next()).done);_iteratorNormalCompletion2=true){var className=_step2.value;if(rule.selectorText.includes(".".concat(className))){return true;}}}catch(err){_didIteratorError2=true;_iteratorError2=err;}finally{try{if(!_iteratorNormalCompletion2&&_iterator2["return"]!=null){_iterator2["return"]();}}finally{if(_didIteratorError2){throw _iteratorError2;}}}}}catch(err){_didIteratorError=true;_iteratorError=err;}finally{try{if(!_iteratorNormalCompletion&&_iterator["return"]!=null){_iterator["return"]();}}finally{if(_didIteratorError){throw _iteratorError;}}}return false;};/** * Checks if a single element uses the custom property via inline styles or class-based rules. * * @param {HTMLElement} el The element to check. * @param {string} customProp The CSS custom property to search for. * @returns {boolean} True if the element uses the custom property. */var elementUsesCustomProp=function elementUsesCustomProp(el,customProp){var _el$getAttribute;if(!(el instanceof Element))return false;var styleAttr=(_el$getAttribute=el.getAttribute)===null||_el$getAttribute===void 0?void 0:_el$getAttribute.call(el,'style');if(styleAttr&&styleAttr.includes("var(".concat(customProp,")"))){return true;}var classList=Array.from(el.classList||[]);return classList.length>0&&classNamesUseCustomProp(classList,customProp);};/** * Checks if an element or any of its descendants use the custom property. * * @param {HTMLElement} el The root element to inspect. * @param {string} customProp The CSS custom property to look for. * @returns {boolean} True if the element or any descendant uses the custom property. */var elementOrDescendantsUseCustomProp=function elementOrDescendantsUseCustomProp(el,customProp){// Check if the element itself uses the custom property if(elementUsesCustomProp(el,customProp))return true;var descendants=el.querySelectorAll('*');// eslint-disable-next-line no-restricted-syntax var _iteratorNormalCompletion3=true;var _didIteratorError3=false;var _iteratorError3=undefined;try{for(var _iterator3=descendants[Symbol.iterator](),_step3;!(_iteratorNormalCompletion3=(_step3=_iterator3.next()).done);_iteratorNormalCompletion3=true){var node=_step3.value;// eslint-disable-next-line no-continue if(!(node instanceof Element))continue;if(elementUsesCustomProp(node,customProp))return true;}}catch(err){_didIteratorError3=true;_iteratorError3=err;}finally{try{if(!_iteratorNormalCompletion3&&_iterator3["return"]!=null){_iterator3["return"]();}}finally{if(_didIteratorError3){throw _iteratorError3;}}}return false;};/** * Returns footer entries that do NOT have safe area insets applied, * either on themselves or in any of their descendants. * * @param {HTMLElement[]} footerElements The footer elements to check. * @returns {HTMLElement[]} An array of direct children that do not use safe area insets. */var getFooterEntriesWithoutSafeAreaInsets=function getFooterEntriesWithoutSafeAreaInsets(footerElements){return footerElements.filter(function(child){return!elementOrDescendantsUseCustomProp(child,'--safe-area-inset-bottom');});};/** * Searches for footer elements that do not have safe area insets applied, and adds a fallback. * @param {HTMLElement} footerEl The footer element whose children are to be checked. */export var handleSafeAreaInsets=function handleSafeAreaInsets(footerEl){if(!footerEl||!(footerEl instanceof HTMLElement)){return;}var directChildren=Array.from(footerEl.children);// Filter out elements that where already handled before var childrenToInspect=directChildren.filter(function(child){return child.getAttribute('data-has-safe-area-inset')!=='true';}).filter(function(child){return child.getAttribute(DATA_IGNORED)!=='true';});// Detect footer elements without safe area insets var childrenWithoutInsets=getFooterEntriesWithoutSafeAreaInsets(childrenToInspect);// Apply fallback and mark the elements as handled childrenWithoutInsets.forEach(function(child){child.style.setProperty('padding-bottom','var(--safe-area-inset-bottom)');child.style.setProperty('background-color',getElementBackgroundColor(child));child.setAttribute('data-has-safe-area-inset','true');});if(isDev&&childrenWithoutInsets.length>0){logger.warn('Footer elements without safe area insets detected. Please use the "--safe-area-inset-bottom" CSS custom property for bottom insets.',childrenWithoutInsets);}// Mark all other elements which already had insets as handled directChildren.filter(function(child){return childrenWithoutInsets.indexOf(child)===-1;}).forEach(function(child){if(!child.hasAttribute('data-has-safe-area-inset')){child.setAttribute('data-has-safe-area-inset','true');}});};var style=document.documentElement.style;/** * Update the footer height custom property * @param {number} height height */export var updateFooterHeight=function updateFooterHeight(height){// The TabBar is positioned with `position: fixed`, so it doesn’t contribute to the measured // height of the Footer. Additionally, it’s sometimes animated in/out, which makes dynamic // measurement via JavaScript more complex and error-prone. // // To simplify everything, we include the --tabbar-height CSS custom property to the calculation // of the --footer-height value. var footerHeight="max(".concat(height,"px, var(--tabbar-height, 0px))");if(style.getPropertyValue('--footer-height')!==footerHeight){style.setProperty('--footer-height',footerHeight);}// The View component wraps every app page and applies a bottom offset to centrally manage // safe area insets across screens. // // If the measured footer height is > 0px, it means the footer is rendering content that already // accounts for the safe area, so no additional offset is needed. // // If the measured footer height is 0px, the footer is either empty or only contains the tab bar // (which is measured via the CSS variable --footer-height). In that case, we still need to apply // an offset equal to the larger of the bottom safe area inset or the tab bar height. var pageContentOffset=height===0?'max(var(--footer-height), var(--safe-area-inset-bottom))':'0px';if(style.getPropertyValue('--page-content-offset-bottom')!==pageContentOffset){style.setProperty('--page-content-offset-bottom',pageContentOffset);}};