@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
369 lines (360 loc) • 12.2 kB
JavaScript
export function isBody(elem) {
return elem === document.body;
}
export function isTextNode(elem) {
return elem && elem.nodeType === 3;
}
/**
* Decides if given fitHeight fits below or above the target taking boundaries into account.
*/
export function getVerticalPlacement(target, boundariesElement, fitHeight, alignY, forcePlacement, preventOverflow) {
if (forcePlacement && alignY) {
return alignY;
}
if (!fitHeight) {
return 'bottom';
}
if (isTextNode(target)) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target = target.parentElement;
}
const boundariesClientRect = boundariesElement.getBoundingClientRect();
const {
height: boundariesHeight
} = boundariesClientRect;
const boundariesTop = isBody(boundariesElement) ? 0 : boundariesClientRect.top;
const {
top: targetTop,
height: targetHeight
} = target.getBoundingClientRect();
const spaceAbove = targetTop - (boundariesTop - boundariesElement.scrollTop);
const spaceBelow = boundariesTop + boundariesHeight - (targetTop + targetHeight);
// Force vertical placement to bottom if the space above doesn't accomodate the fitHeight
if (preventOverflow) {
if (spaceAbove <= fitHeight) {
return 'bottom';
}
}
if (spaceBelow >= fitHeight || spaceBelow >= spaceAbove) {
return 'bottom';
}
return 'top';
}
/**
* Decides if given fitWidth fits to the left or to the right of the target taking boundaries into account.
*/
export function getHorizontalPlacement(target, boundariesElement, fitWidth, alignX, forcePlacement, preventOverflow) {
// force placement unless preventOverflow is enabled
if (forcePlacement && alignX && !preventOverflow) {
return alignX;
}
if (!fitWidth) {
return alignX || 'left';
}
if (isTextNode(target)) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target = target.parentElement;
}
const {
left: targetLeft,
width: targetWidth
} = target.getBoundingClientRect();
const {
left: boundariesLeft,
width: boundariesWidth
} = boundariesElement.getBoundingClientRect();
const spaceLeft = targetLeft - boundariesLeft + targetWidth;
const spaceRight = boundariesLeft + boundariesWidth - targetLeft;
if (alignX && spaceLeft > fitWidth && spaceRight > fitWidth) {
return alignX;
} else if (spaceRight >= fitWidth || spaceRight >= spaceLeft && !alignX) {
return 'left';
}
return 'right';
}
export function calculatePlacement(target, boundariesElement, fitWidth, fitHeight, alignX, alignY, forcePlacement, preventOverflow) {
return [getVerticalPlacement(target, boundariesElement, fitHeight, alignY, forcePlacement, preventOverflow), getHorizontalPlacement(target, boundariesElement, fitWidth, alignX, forcePlacement, preventOverflow)];
}
const calculateHorizontalPlacement = ({
placement,
targetLeft,
targetRight,
targetWidth,
isPopupParentBody,
popupOffsetParentLeft,
popupOffsetParentRight,
popupOffsetParentScrollLeft,
popupOffsetParentClientWidth,
popupClientWidth,
offset,
allowOutOfBounds = false,
minPopupMargin
}) => {
const position = {};
if (placement === 'left') {
position.left = Math.ceil(targetLeft - popupOffsetParentLeft + (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0]);
} else if (placement === 'center') {
position.left = Math.ceil(targetLeft - popupOffsetParentLeft + (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0] + targetWidth / 2 - popupClientWidth / 2);
} else if (placement === 'end') {
const left = Math.ceil(targetRight - popupOffsetParentLeft + (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0]);
position.left = left;
} else {
position.right = Math.ceil(popupOffsetParentRight - targetRight - (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0]);
}
if (!allowOutOfBounds) {
if (position.left !== undefined) {
position.left = getPopupXInsideParent(position.left, popupClientWidth, popupOffsetParentClientWidth, minPopupMargin);
}
if (position.right !== undefined) {
position.right = getPopupXInsideParent(position.right, popupClientWidth, popupOffsetParentClientWidth, minPopupMargin);
}
}
return position;
};
const getPopupXInsideParent = (x, popupClientWidth, popupOffsetParentClientWidth, minPopupMargin = 1) => {
// prevent going too far right
if (popupOffsetParentClientWidth < x + popupClientWidth) {
x = popupOffsetParentClientWidth - popupClientWidth - minPopupMargin;
}
// prevent going too far left
return Math.max(minPopupMargin, x);
};
const calculateVerticalStickBottom = ({
target,
targetTop,
targetHeight,
popup,
offset,
position,
boundariesElement,
scrollableElement
}) => {
const scrollParent = scrollableElement || findOverflowScrollParent(target) || boundariesElement;
const newPos = {
...position
};
if (scrollParent) {
const topOffsetTop = targetTop - scrollParent.getBoundingClientRect().top;
const targetEnd = targetHeight + topOffsetTop;
if (scrollParent.clientHeight - targetEnd <= popup.clientHeight + offset[1] * 2 && topOffsetTop < scrollParent.clientHeight) {
const scroll = targetEnd - scrollParent.clientHeight + offset[1] * 2;
let top = newPos.top || 0;
top = top - (scroll + popup.clientHeight);
newPos.top = top;
}
}
return newPos;
};
const calculateVerticalStickTop = ({
target,
targetTop,
targetHeight,
popupOffsetParentHeight,
popupOffsetParent,
offset,
position,
placement,
boundariesElement,
scrollableElement
}) => {
const scrollParent = scrollableElement || findOverflowScrollParent(target) || boundariesElement;
const newPos = {
...position
};
if (scrollParent) {
const {
top: scrollParentTop
} = scrollParent.getBoundingClientRect();
const topBoundary = targetTop - scrollParentTop;
const scrollParentScrollTop = scrollParent.scrollTop;
if (topBoundary < 0) {
const isBelowNodeBoundary = targetTop + (scrollParentScrollTop - scrollParentTop) + targetHeight + offset[1] < scrollParentScrollTop;
if (placement === 'top') {
if (isBelowNodeBoundary) {
newPos.bottom = popupOffsetParentHeight - (topBoundary + popupOffsetParent.scrollTop + targetHeight);
} else {
newPos.bottom = topBoundary + (newPos.bottom || 0);
}
}
if (placement === 'start') {
if (isBelowNodeBoundary) {
newPos.top = topBoundary + popupOffsetParent.scrollTop + targetHeight;
} else {
newPos.top = Math.abs(topBoundary) + (newPos.top || 0) + offset[1];
}
}
}
}
return newPos;
};
const calculateVerticalPlacement = ({
placement,
targetTop,
targetHeight,
isPopupParentBody,
popupOffsetParentHeight,
popupOffsetParentTop,
popupOffsetParentScrollTop,
borderBottomWidth,
offset
}) => {
const position = {};
if (placement === 'top') {
position.bottom = Math.ceil(popupOffsetParentHeight - (targetTop - popupOffsetParentTop) - (isPopupParentBody ? 0 : popupOffsetParentScrollTop) - borderBottomWidth + offset[1]);
} else if (placement === 'start') {
position.top = Math.ceil(targetTop - popupOffsetParentTop - offset[1] + (isPopupParentBody ? 0 : popupOffsetParentScrollTop));
} else {
const top = Math.ceil(targetTop - popupOffsetParentTop + targetHeight + (isPopupParentBody ? 0 : popupOffsetParentScrollTop) - borderBottomWidth + offset[1]);
position.top = top;
}
return position;
};
/**
* Calculates relative coordinates for placing popup along with the target.
* Uses placement from calculatePlacement.
*/
export function calculatePosition({
placement,
target,
popup,
offset,
stick,
allowOutOfBounds = false,
rect,
boundariesElement,
minPopupMargin,
scrollableElement
}) {
let position = {};
if (!target || !popup || !popup.offsetParent || !isHTMLElementNode(popup.offsetParent)) {
return position;
}
if (isTextNode(target)) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target = target.parentElement;
}
const popupOffsetParent = popup.offsetParent;
const offsetParentStyle = popupOffsetParent.style;
let borderBottomWidth = 0;
if (offsetParentStyle && offsetParentStyle.borderBottomWidth) {
borderBottomWidth = parseInt(offsetParentStyle.borderBottomWidth, 10);
}
const [verticalPlacement, horizontalPlacement] = placement;
const {
top: popupOffsetParentTop,
left: popupOffsetParentLeft,
right: popupOffsetParentRight,
height: popupOffsetParentHeight
} = rect ? rect : popupOffsetParent.getBoundingClientRect();
// Calculate scrollbar dimensions to adjust positions
// clientWidth/Height excludes scrollbars, offsetWidth/Height includes them
const scrollbarWidth = popupOffsetParent.offsetWidth - popupOffsetParent.clientWidth;
const scrollbarHeight = popupOffsetParent.offsetHeight - popupOffsetParent.clientHeight;
const {
top: targetTop,
left: targetLeft,
right: targetRight,
height: targetHeight,
width: targetWidth
} = target.getBoundingClientRect();
const isPopupParentBody = isBody(popupOffsetParent);
const verticalPosition = calculateVerticalPlacement({
placement: verticalPlacement,
targetTop,
isPopupParentBody,
popupOffsetParentHeight: popupOffsetParentHeight - (isPopupParentBody ? 0 : scrollbarHeight),
popupOffsetParentTop,
popupOffsetParentScrollTop: popupOffsetParent.scrollTop || 0,
targetHeight,
borderBottomWidth,
offset
});
position = {
...position,
...verticalPosition
};
if ((verticalPlacement === 'top' || verticalPlacement === 'start') && stick) {
position = calculateVerticalStickTop({
target,
targetTop,
targetHeight,
popupOffsetParentHeight,
popupOffsetParent,
popup,
offset,
position,
placement: verticalPlacement,
boundariesElement,
scrollableElement
});
}
if (verticalPlacement !== 'top' && verticalPlacement !== 'start' && stick) {
position = calculateVerticalStickBottom({
target,
targetTop,
targetHeight,
popup,
offset,
position,
boundariesElement,
scrollableElement
});
}
const horizontalPosition = calculateHorizontalPlacement({
placement: horizontalPlacement,
targetLeft,
targetRight,
targetWidth,
isPopupParentBody,
popupOffsetParentLeft,
popupOffsetParentRight: popupOffsetParentRight - (isPopupParentBody ? 0 : scrollbarWidth),
popupOffsetParentScrollLeft: popupOffsetParent.scrollLeft || 0,
popupOffsetParentClientWidth: popup.offsetParent.clientWidth,
popupClientWidth: popup.clientWidth || 0,
offset,
allowOutOfBounds,
minPopupMargin
});
position = {
...position,
...horizontalPosition
};
return position;
}
export function validatePosition(popup) {
// popup.offsetParent does not exist if the popup element is not mounted
if (!popup || !popup.offsetParent) {
return false;
}
return true;
}
/**
* Traverse DOM Tree upwards looking for popup parents with "overflow: scroll".
*/
export function findOverflowScrollParent(popup) {
let parent = popup;
if (!parent) {
return false;
}
// Ignored via go/ees005
// eslint-disable-next-line no-cond-assign
while (parent = parent.parentElement) {
// IE11 on Window 8 doesn't show styles from CSS when accessing through element.style property.
const style = window.getComputedStyle(parent);
if (style.overflow === 'scroll' || style.overflowX === 'scroll' || style.overflowY === 'scroll' || parent.classList.contains('fabric-editor-popup-scroll-parent')) {
return parent;
}
}
return false;
}
// Helper function to check if the passed node is of Element class
function isElementNode(node) {
return node.nodeType === 1;
}
// Helper function to check if the passed node is of HTMLElement class
function isHTMLElementNode(node) {
return isElementNode(node) && node.namespaceURI === 'http://www.w3.org/1999/xhtml';
}