@shopgate/engage
Version:
Shopgate's ENGAGE library.
75 lines • 9.72 kB
JavaScript
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);}};