UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

369 lines (360 loc) • 12.2 kB
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'; }