@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
230 lines (229 loc) • 9.5 kB
JavaScript
import { Rect } from '../../esl-utils/dom/rect';
/**
* Checks that the position along the horizontal axis
* @param position - name of position
*/
export function isOnHorizontalAxis(position) {
return ['left', 'right'].includes(position.split(/\s+/)[0]);
}
/**
* Checks whether the specified position corresponds to the starting side
* @param position - name of position
*/
function isStartingSide(placement) {
return ['left', 'top'].includes(placement);
}
/**
* Gets the name of the dimension along the axis of the specified position
* @param position - name of position
* @param alter - should it be the opposite dimension?
*/
function getDimensionName(position, alter = false) {
const isHorizontal = isOnHorizontalAxis(position);
return (alter ? !isHorizontal : isHorizontal) ? 'width' : 'height';
}
/**
* Gets the name of the position where the arrow should be placed
* @param cfg - popup position config
* @param isOpposite - should it be the opposite position?
*/
function getPlacedAt(cfg, isOpposite = false) {
const position = isOpposite ? getOppositePlacement(cfg.placement) : cfg.placement;
return `${position}${cfg.hasInnerOrigin ? '-inner' : ''}`;
}
/**
* Calculates the position of the popup on the major axis
* @param cfg - popup position config
*/
function calcPopupPositionByMajorAxis(cfg) {
const { placement, inner, element, hasInnerOrigin } = cfg;
const coord = inner[placement];
const size = element[getDimensionName(placement)];
return isStartingSide(placement)
? (hasInnerOrigin ? coord : coord - size)
: (hasInnerOrigin ? coord - size : coord);
}
/**
* Calculates the position of the popup on the minor axis
* @param cfg - popup position config
*/
function calcPopupPositionByMinorAxis(cfg) {
const { placement, arrow, offsetTetherRatio, marginTether } = cfg;
const dimensionName = getDimensionName(placement, true);
return calcTetherPlacement(cfg) - arrow[dimensionName] / 2 - marginTether - calcUsableSizeForArrow(cfg, dimensionName) * offsetTetherRatio;
}
/**
* Gets the coordinate for tether placement based on the alignment and placement.
* @param cfg - popup position config
*/
function getTetherPlacementCoord(cfg) {
if (cfg.alignment === 'start')
return isOnHorizontalAxis(cfg.placement) ? 'y' : 'x';
if (cfg.alignment === 'end')
return isOnHorizontalAxis(cfg.placement) ? 'bottom' : 'right';
return isOnHorizontalAxis(cfg.placement) ? 'cy' : 'cx';
}
/**
* Calculates the tether placement coordinate on the minor axis.
* @param cfg - popup position config
*/
function calcTetherPlacement(cfg) {
return cfg.trigger[getTetherPlacementCoord(cfg)] + cfg.offsetPlacement;
}
/**
* Calculates Rect for given popup position config.
* @param cfg - popup position config
* */
function calcPopupBasicRect(cfg) {
const { width, height } = cfg.element;
const coordForMajor = calcPopupPositionByMajorAxis(cfg);
const coordForMinor = calcPopupPositionByMinorAxis(cfg);
return isOnHorizontalAxis(cfg.placement)
? new Rect(coordForMajor, coordForMinor, width, height)
: new Rect(coordForMinor, coordForMajor, width, height);
}
/**
* Calculates position for all sub-parts of popup for given popup position config.
* @param cfg - popup position config
* */
function calcBasicPosition(cfg) {
const popup = calcPopupBasicRect(cfg);
const arrow = {
x: calcArrowPosition(cfg, 'width'),
y: calcArrowPosition(cfg, 'height'),
};
return { arrow, popup, placedAt: getPlacedAt(cfg) };
}
/**
* Gets opposite placement.
* @param placement - name of placement
* */
function getOppositePlacement(placement) {
return ({
top: 'bottom',
left: 'right',
right: 'left',
bottom: 'top'
}[placement] || placement);
}
/**
* Checks and updates popup and arrow positions to fit on major axis.
* @param cfg - popup position config
* @param value - current popup's position value
* @returns updated popup position value
* */
function fitOnMajorAxis(cfg, value) {
if (!['fit', 'fit-major'].includes(cfg.behavior))
return value;
const { placement, hasInnerOrigin, outer } = cfg;
const intersectionRatio = cfg.intersectionRatio[hasInnerOrigin ? getOppositePlacement(placement) : placement] || 0;
const valueToCheck = hasInnerOrigin ? cfg.inner : value.popup;
const isComingOut = isStartingSide(placement)
? valueToCheck[placement] < outer[placement]
: outer[placement] < valueToCheck[placement];
const isRequireAdjusting = hasInnerOrigin
? intersectionRatio === 0 && isComingOut
: intersectionRatio > 0 || isComingOut;
return isRequireAdjusting ? adjustAlongMajorAxis(cfg, value) : value;
}
/**
* Updates popup and arrow positions to fit on major axis.
* @param cfg - popup position config
* @param value - current popup's position value
* @returns updated popup position value
* */
function adjustAlongMajorAxis(cfg, value) {
const oppositeConfig = Object.assign(Object.assign({}, cfg), { placement: getOppositePlacement(cfg.placement) });
const { x, y, width, height } = value.popup;
const adjustedCoord = calcPopupPositionByMajorAxis(oppositeConfig);
const popup = isOnHorizontalAxis(cfg.placement)
? new Rect(adjustedCoord, y, width, height)
: new Rect(x, adjustedCoord, width, height);
return Object.assign(Object.assign({}, value), { popup, placedAt: getPlacedAt(cfg, true) });
}
/**
* Calculates adjust for popup position to fit container bounds
* @param cfg - popup position config
* @param diffCoord - distance between the popup and the outer (container) bounding
* @param arrowCoord - coordinate of the arrow
* @param isStart - should it rely on the starting side?
* @returns adjustment value for the coordinates of the arrow and the popup
*/
function adjustAlignmentBySide(cfg, diffCoord, arrowCoord, isStart) {
let arrowAdjust = 0;
if (isStart ? diffCoord < 0 : diffCoord > 0) {
arrowAdjust = diffCoord;
const newCoord = arrowCoord + arrowAdjust;
const dimension = getDimensionName(cfg.placement, true);
const arrowLimit = cfg.marginTether + (isStart ? 0 : calcUsableSizeForArrow(cfg, dimension));
if (isStart ? newCoord < arrowLimit : newCoord > arrowLimit) {
arrowAdjust -= newCoord - arrowLimit;
}
}
return arrowAdjust;
}
/**
* Sets up the configuration for adjusting position along the minor axis
* @param cfg - popup position config
* @param popup - current popup's position value
* @returns configuration for adjusting position along the minor axis
*/
function setupAlignmentBySide(cfg, popup) {
const isHorizontal = isOnHorizontalAxis(cfg.placement);
const start = isHorizontal ? 'y' : 'x';
const end = isHorizontal ? 'bottom' : 'right';
const dimension = getDimensionName(cfg.placement, true);
const isOutAtStart = popup[start] < cfg.outer[start];
const isOutAtEnd = popup[end] > cfg.outer[end];
const isWider = cfg.outer[dimension] < cfg.element[dimension];
return { isHorizontal, start, end, isOutAtStart, isOutAtEnd, isWider };
}
/**
* Updates popup and arrow positions to fit on minor axis.
* @param cfg - popup position config
* @param value - current popup's position value
* @returns updated popup position value
* */
function fitOnMinorAxis(cfg, value) {
if (!['fit', 'fit-minor'].includes(cfg.behavior))
return value;
const { popup, arrow } = value;
const { isHorizontal, start, end, isOutAtStart, isOutAtEnd, isWider } = setupAlignmentBySide(cfg, popup);
// nothing to do when there is no outing
if (!isOutAtStart && !isOutAtEnd)
return value;
// start-side adjusting happens if there is only start-side outing or LTR content direction
const isStarting = isOutAtStart && (!isOutAtEnd || !cfg.isRTL);
// the side for calculating the distance between the popup and the outer (container) bounding should be:
// - when the popup is wider than the container the diff side should depend on the text direction
// (start side for LTR, end side for RTL)
// - else we should choose start side if start-side outing or end side if end-side outing
const diffSide = (isWider ? !cfg.isRTL : isStarting) ? start : end;
const diff = popup[diffSide] - cfg.outer[diffSide];
const shift = adjustAlignmentBySide(cfg, diff, arrow[start], isStarting);
arrow[start] += shift;
return Object.assign(Object.assign({}, value), { popup: isHorizontal ? popup.shift(0, -shift) : popup.shift(-shift, 0), arrow });
}
/**
* Calculates the usable size available for the arrow
* @param cfg - popup position config
* @param dimensionName - the name of dimension (height or width)
*/
function calcUsableSizeForArrow(cfg, dimensionName) {
return cfg.element[dimensionName] - cfg.arrow[dimensionName] - 2 * cfg.marginTether;
}
/**
* Calculates the position of the arrow on the minor axis
* @param cfg - popup position config
* @param dimensionName - the name of dimension (height or width)
*/
function calcArrowPosition(cfg, dimensionName) {
return cfg.marginTether + calcUsableSizeForArrow(cfg, dimensionName) * cfg.offsetTetherRatio;
}
/**
* Calculates popup and arrow popup positions.
* @param cfg - popup position config
* */
export function calcPopupPosition(cfg) {
return fitOnMinorAxis(cfg, fitOnMajorAxis(cfg, calcBasicPosition(cfg)));
}