@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
213 lines (212 loc) • 8.86 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);
}
/**
* Checks whether the specified position corresponds to the starting side
* @param position - name of position
*/
function isStartingSide(position) {
return ['left', 'top'].includes(position);
}
/**
* 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 ? getOppositePosition(cfg.position) : cfg.position;
return `${position}${cfg.hasInnerOrigin ? '-inner' : ''}`;
}
/**
* Calculates the position of the popup on the major axis
* @param cfg - popup position config
*/
function calcPopupPositionByMajorAxis(cfg) {
const { position, inner, element, hasInnerOrigin } = cfg;
const coord = inner[position];
const size = element[getDimensionName(position)];
return isStartingSide(position)
? (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 { position, inner, arrow, marginArrow, offsetArrowRatio } = cfg;
const centerPosition = inner[isOnHorizontalAxis(position) ? 'cy' : 'cx'];
const dimensionName = getDimensionName(position, true);
return centerPosition - arrow[dimensionName] / 2 - marginArrow - calcUsableSizeForArrow(cfg, dimensionName) * offsetArrowRatio;
}
/**
* 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.position)
? 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 position.
* @param position - name of position
* */
function getOppositePosition(position) {
return ({
top: 'bottom',
left: 'right',
right: 'left',
bottom: 'top'
}[position] || position);
}
/**
* 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 { position, hasInnerOrigin, outer } = cfg;
const intersectionRatio = cfg.intersectionRatio[hasInnerOrigin ? getOppositePosition(position) : position] || 0;
const valueToCheck = hasInnerOrigin ? cfg.inner : value.popup;
const isComingOut = isStartingSide(position)
? valueToCheck[position] < outer[position]
: outer[position] < valueToCheck[position];
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), { position: getOppositePosition(cfg.position) });
const { x, y, width, height } = value.popup;
const adjustedCoord = calcPopupPositionByMajorAxis(oppositeConfig);
const popup = isOnHorizontalAxis(cfg.position)
? 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.position, true);
const arrowLimit = cfg.marginArrow + (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.position);
const start = isHorizontal ? 'y' : 'x';
const end = isHorizontal ? 'bottom' : 'right';
const dimension = getDimensionName(cfg.position, 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.marginArrow;
}
/**
* 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.marginArrow + calcUsableSizeForArrow(cfg, dimensionName) * cfg.offsetArrowRatio;
}
/**
* Calculates popup and arrow popup positions.
* @param cfg - popup position config
* */
export function calcPopupPosition(cfg) {
return fitOnMinorAxis(cfg, fitOnMajorAxis(cfg, calcBasicPosition(cfg)));
}