UNPKG

@logo-elements/overlay

Version:

<logo-elements-overlay> is a Web Component meant for internal use in Logo Elements web components to overlay items.

253 lines (214 loc) 9.15 kB
/** * @license * Copyright LOGO YAZILIM SANAYİ VE TİCARET A.Ş. All Rights Reserved. * * Save to the extent permitted by law, you may not use, copy, modify, * distribute or create derivative works of this material or any part * of it without the prior written consent of LOGO YAZILIM SANAYİ VE TİCARET A.Ş. Limited. * Any reproduction of this material must contain this notice. */ const PROP_NAMES_VERTICAL = { start: 'top', end: 'bottom' }; const PROP_NAMES_HORIZONTAL = { start: 'left', end: 'right' }; /** * @polymerMixin */ export const PositionMixin = (superClass) => class PositionMixin extends superClass { static get properties() { return { /** * The element next to which this overlay should be aligned. * The position of the overlay relative to the positionTarget can be adjusted * with properties `horizontalAlign`, `verticalAlign`, `noHorizontalOverlap` * and `noVerticalOverlap`. */ positionTarget: { type: Object, value: null }, /** * When `positionTarget` is set, this property defines whether to align the overlay's * left or right side to the target element by default. * Possible values are `start` and `end`. * RTL is taken into account when interpreting the value. * The overlay is automatically flipped to the opposite side when it doesn't fit into * the default side defined by this property. */ horizontalAlign: { type: String, value: 'start' }, /** * When `positionTarget` is set, this property defines whether to align the overlay's * top or bottom side to the target element by default. * Possible values are `top` and `bottom`. * The overlay is automatically flipped to the opposite side when it doesn't fit into * the default side defined by this property. */ verticalAlign: { type: String, value: 'top' }, /** * When `positionTarget` is set, this property defines whether the overlay should overlap * the target element in the x-axis, or be positioned right next to it. */ noHorizontalOverlap: { type: Boolean, value: false }, /** * When `positionTarget` is set, this property defines whether the overlay should overlap * the target element in the y-axis, or be positioned right above/below it. */ noVerticalOverlap: { type: Boolean, value: false } }; } static get observers() { return [ '__positionSettingsChanged(positionTarget, horizontalAlign, verticalAlign, noHorizontalOverlap, noVerticalOverlap)', '__overlayOpenedChanged(opened)' ]; } constructor() { super(); this.__boundUpdatePosition = this._updatePosition.bind(this); } __overlayOpenedChanged(opened) { // Toggle the event listeners that cause the overlay to update its position ['scroll', 'resize'].forEach((eventName) => { if (opened) { window.addEventListener(eventName, this.__boundUpdatePosition); } else { window.removeEventListener(eventName, this.__boundUpdatePosition); } }); if (opened) { const computedStyle = getComputedStyle(this); if (!this.__margins) { this.__margins = {}; ['top', 'bottom', 'left', 'right'].forEach((propName) => { this.__margins[propName] = parseInt(computedStyle[propName], 10); }); } this.setAttribute('dir', computedStyle.direction); this._updatePosition(); // Schedule another position update (to cover virtual keyboard opening for example) requestAnimationFrame(() => this._updatePosition()); } } get __isRTL() { return this.getAttribute('dir') === 'rtl'; } __positionSettingsChanged() { this._updatePosition(); } _updatePosition() { if (!this.positionTarget || !this.opened) { return; } const targetRect = this.positionTarget.getBoundingClientRect(); // Detect the desired alignment and update the layout accordingly const shouldAlignStartVertically = this.__shouldAlignStartVertically(targetRect); this.style.justifyContent = shouldAlignStartVertically ? 'flex-start' : 'flex-end'; const shouldAlignStartHorizontally = this.__shouldAlignStartHorizontally(targetRect, this.__isRTL); const flexStart = (!this.__isRTL && shouldAlignStartHorizontally) || (this.__isRTL && !shouldAlignStartHorizontally); this.style.alignItems = flexStart ? 'flex-start' : 'flex-end'; // Get the overlay rect after possible overlay alignment changes const overlayRect = this.getBoundingClientRect(); // Obtain vertical positioning properties const verticalProps = this.__calculatePositionInOneDimension( targetRect, overlayRect, this.noVerticalOverlap, PROP_NAMES_VERTICAL, this, shouldAlignStartVertically ); // Obtain horizontal positioning properties const horizontalProps = this.__calculatePositionInOneDimension( targetRect, overlayRect, this.noHorizontalOverlap, PROP_NAMES_HORIZONTAL, this, shouldAlignStartHorizontally ); // Apply the positioning properties to the overlay Object.assign(this.style, verticalProps, horizontalProps); this.toggleAttribute('bottom-aligned', !shouldAlignStartVertically); this.toggleAttribute('top-aligned', shouldAlignStartVertically); this.toggleAttribute('end-aligned', !flexStart); this.toggleAttribute('start-aligned', flexStart); } __shouldAlignStartHorizontally(targetRect, rtl) { // Using previous size to fix a case where window resize may cause the overlay to be squeezed // smaller than its current space before the fit-calculations. const contentWidth = Math.max(this.__oldContentWidth || 0, this.$.overlay.offsetWidth); this.__oldContentWidth = this.$.overlay.offsetWidth; const viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth); const defaultAlignLeft = (!rtl && this.horizontalAlign === 'start') || (rtl && this.horizontalAlign === 'end'); return this.__shouldAlignStart( targetRect, contentWidth, viewportWidth, this.__margins, defaultAlignLeft, this.noHorizontalOverlap, PROP_NAMES_HORIZONTAL ); } __shouldAlignStartVertically(targetRect) { // Using previous size to fix a case where window resize may cause the overlay to be squeezed // smaller than its current space before the fit-calculations. const contentHeight = Math.max(this.__oldContentHeight || 0, this.$.overlay.offsetHeight); this.__oldContentHeight = this.$.overlay.offsetHeight; const viewportHeight = Math.min(window.innerHeight, document.documentElement.clientHeight); const defaultAlignTop = this.verticalAlign === 'top'; return this.__shouldAlignStart( targetRect, contentHeight, viewportHeight, this.__margins, defaultAlignTop, this.noVerticalOverlap, PROP_NAMES_VERTICAL ); } __shouldAlignStart(targetRect, contentSize, viewportSize, margins, defaultAlignStart, noOverlap, propNames) { const spaceForStartAlignment = viewportSize - targetRect[noOverlap ? propNames.end : propNames.start] - margins[propNames.end]; const spaceForEndAlignment = targetRect[noOverlap ? propNames.start : propNames.end] - margins[propNames.start]; const spaceForDefaultAlignment = defaultAlignStart ? spaceForStartAlignment : spaceForEndAlignment; const spaceForOtherAlignment = defaultAlignStart ? spaceForEndAlignment : spaceForStartAlignment; const shouldGoToDefaultSide = spaceForDefaultAlignment > spaceForOtherAlignment || spaceForDefaultAlignment > contentSize; return defaultAlignStart === shouldGoToDefaultSide; } /** * Returns an object with CSS position properties to set, * e.g. { top: "100px", bottom: "" } */ __calculatePositionInOneDimension(targetRect, overlayRect, noOverlap, propNames, overlay, shouldAlignStart) { const cssPropNameToSet = shouldAlignStart ? propNames.start : propNames.end; const cssPropNameToClear = shouldAlignStart ? propNames.end : propNames.start; const currentValue = parseFloat(overlay.style[cssPropNameToSet] || getComputedStyle(overlay)[cssPropNameToSet]); const diff = overlayRect[shouldAlignStart ? propNames.start : propNames.end] - targetRect[noOverlap === shouldAlignStart ? propNames.end : propNames.start]; return { [cssPropNameToSet]: currentValue + diff * (shouldAlignStart ? -1 : 1) + 'px', [cssPropNameToClear]: '' }; } };