/** @license Copyright (c) 2022 trading_peter This program is available under Apache License Version 2.0 */ /** * Helps to position elements relative to each other. */ export const Position = function(superClass) { return class extends superClass { _posFixed(anchor, el, options) { options = Object.assign({ valign: 'top', halign: 'middle', spacing: 0, // When true, clamp against the element's containing-block rect // instead of the viewport. Enable for popups rendered under an // ancestor with transform / filter / perspective / will-change / // contain:layout|paint — there, position: fixed resolves against // that ancestor and the viewport is the wrong clamp edge. clampToContainer: false }, options); let top, left, fixLeft = 0, fixTop = 0, compLeft = 0, compTop = 0; el.style.position = 'fixed'; el.style.zIndex = 1001; // Reset max-height and overflow in case the element has been positioned before. el.style.maxHeight = ''; el.style.overflowY = ''; // Test if the target is in a different stacking context. el.style.left = '0px'; el.style.top = '0px'; const elRect = el.getBoundingClientRect(); if (elRect.left > 0 || elRect.top > 0) { fixLeft = elRect.left; fixTop = elRect.top; } // Resolve the clamp rect. Default is the viewport; when the consumer // opts in, prefer the containing-block rect (falling back to viewport // if no qualifying ancestor exists — e.g. the popup is viewport-fixed). const viewport = { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight }; const clampRect = options.clampToContainer ? (this._containingBlockRect(el) || viewport) : viewport; const anchorRect = anchor.getBoundingClientRect(); if (options.valign === 'top') { top = anchorRect.top - elRect.height - options.spacing; // Move popup down a little bit if there isn't enough room over the anchor. if (top < clampRect.top) { compTop = clampRect.top - top; top = clampRect.top; } } if (options.valign === 'bottom') { top = anchorRect.top + anchorRect.height + options.spacing; // Move popup up a little bit if there isn't enough room under the anchor. if (top + elRect.height > clampRect.bottom) { compTop = top + elRect.height - clampRect.bottom; top -= compTop; } } // Ensure top never goes above the clamp rect's top edge. if (top < clampRect.top) { compTop += clampRect.top - top; top = clampRect.top; } if (options.halign === 'left') { left = anchorRect.left; } if (options.halign === 'middle') { left = anchorRect.left - elRect.width / 2 + anchorRect.width / 2; } if (options.halign === 'right') { left = anchorRect.left + anchorRect.width - elRect.width; } if (left + elRect.width > clampRect.right) { compLeft = left + elRect.width - clampRect.right; left -= compLeft; } if (left < clampRect.left) { compLeft = clampRect.left - left; left = clampRect.left; } // Constrain height to clamp rect — calculate available height from final top. const availableHeight = clampRect.bottom - top; if (elRect.height > availableHeight) { el.style.maxHeight = availableHeight + 'px'; el.style.overflowY = 'auto'; } el.style.top = (top - fixTop) + 'px'; el.style.left = (left - fixLeft) + 'px'; // Return info object about how much the position had to compensate to fit on the page. return { compLeft: compLeft, compTop: compTop }; } /** * Walk up from `el` — across shadow boundaries — to find the nearest * ancestor that establishes a containing block for fixed-positioned * descendants (CSS Transforms / Filter Effects / Containment specs). * Returns that ancestor's viewport rect, or null when `el` is fixed to * the viewport (no qualifying ancestor). */ _containingBlockRect(el) { let node = el.parentElement || (el.getRootNode && el.getRootNode().host); while (node) { const cs = getComputedStyle(node); if (cs && this._establishesContainingBlock(cs)) { return node.getBoundingClientRect(); } node = node.parentElement || (node.getRootNode && node.getRootNode().host); } return null; } /** * Per CSS, an element becomes the containing block for fixed descendants * when it has a non-`none` transform, perspective, filter, backdrop-filter, * a `will-change` listing any of those, or a `contain` value that includes * layout or paint (the `strict` and `content` shorthands qualify). */ _establishesContainingBlock(cs) { if (cs.transform && cs.transform !== 'none') return true; if (cs.perspective && cs.perspective !== 'none') return true; if (cs.filter && cs.filter !== 'none') return true; if (cs.backdropFilter && cs.backdropFilter !== 'none') return true; if (cs.webkitBackdropFilter && cs.webkitBackdropFilter !== 'none') return true; const wc = cs.willChange; if (wc && wc !== 'auto' && (wc.includes('transform') || wc.includes('perspective') || wc.includes('filter'))) { return true; } const c = cs.contain; if (c && c !== 'none' && (c.includes('layout') || c.includes('paint') || c.includes('strict') || c.includes('content'))) { return true; } return false; } }; }