@gooddata/react-components
Version:
GoodData.UI - A powerful JavaScript library for building analytical applications
217 lines (184 loc) • 7.32 kB
text/typescript
// (C) 2007-2018 GoodData Corporation
import { MenuAlignment } from "../MenuSharedTypes";
export interface IDimensions {
width: number;
height: number;
}
export interface ICoordinates {
left: number;
top: number;
right: number;
bottom: number;
}
export interface IDimensionsAndCoordinates extends IDimensions, ICoordinates {}
export type Dimension = "width" | "height";
export type Direction = "left" | "right" | "top" | "bottom";
export interface IMenuPosition {
left: number;
top: number;
}
export function getViewportDimensionsAndCoords(): IDimensionsAndCoordinates {
const width = window.innerWidth;
const height = window.innerHeight;
const left = window.pageXOffset;
const top = window.pageYOffset;
const right = left + width;
const bottom = top + height;
return { width, height, left, top, right, bottom };
}
export function getElementDimensionsAndCoords(element: HTMLElement): IDimensionsAndCoordinates {
const rect = element.getBoundingClientRect();
const window = getViewportDimensionsAndCoords();
const width = rect.width;
const height = rect.height;
const left = rect.left + window.left;
const top = rect.top + window.top;
const right = left + width;
const bottom = top + height;
return { width, height, left, top, right, bottom };
}
export function getElementDimensions(element: Element): IDimensions {
const rect = element.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
return { width, height };
}
const reverseDirectionMap: { [key in Direction]: Direction } = {
left: "right",
right: "left",
top: "bottom",
bottom: "top",
};
const dimensionMap: { [key in Direction]: Dimension } = {
left: "width",
right: "width",
top: "height",
bottom: "height",
};
export interface IMenuPositionConfig {
toggler: IDimensionsAndCoordinates;
viewport: IDimensionsAndCoordinates;
menu: IDimensions;
alignment: MenuAlignment;
spacing: number;
offset: number;
topLevelMenu: boolean;
}
export function calculateMenuPosition({
toggler,
viewport,
menu,
alignment = ["right", "bottom"],
spacing = 0,
offset = 0,
topLevelMenu = true,
}: IMenuPositionConfig): IMenuPosition {
const sharedArguments = { toggler, viewport, menu, spacing, offset };
const [directionPreferredPrimary, directionPrefferedSecondary] = alignment;
const [primaryCoordinateDirection, primaryCoordinate] = calculatePositionForDirection({
...sharedArguments,
direction: directionPreferredPrimary,
isPrimaryDimension: true,
});
const [secondaryCoordinateDirection, secondaryCoordinate] = calculatePositionForDirection({
...sharedArguments,
direction: directionPrefferedSecondary,
isPrimaryDimension: false,
});
const coordinates: Partial<ICoordinates> = {
[primaryCoordinateDirection]: primaryCoordinate,
[secondaryCoordinateDirection]: secondaryCoordinate,
};
// Convert from left/right+top/bottom coordinates to left+top coordinates
const res: IMenuPosition = {
left:
typeof coordinates.left === "number"
? coordinates.left
: toggler.width - menu.width - coordinates.right,
top:
typeof coordinates.top === "number"
? coordinates.top
: toggler.height - menu.height - coordinates.bottom,
};
// Returned coordinates are relative to toggler.
// - Submenus are positioned relative to the toggler, so we do not do anything.
// - Top menu is inside portal positioned relative to the page, so we convert
// from coords relative to toggler to coords relative to page.
if (topLevelMenu) {
res.left += toggler.left;
res.top += toggler.top;
}
return res;
}
function calculatePositionForDirection({
toggler,
viewport,
menu,
direction,
spacing,
offset,
isPrimaryDimension,
}: {
toggler: IDimensionsAndCoordinates;
viewport: IDimensionsAndCoordinates;
menu: IDimensions;
direction: Direction;
spacing: number;
offset: number;
isPrimaryDimension: boolean;
}): [Direction, number] {
// Toggler and viewport coordinates are absolute to the page.
// Returned coordinates are relative to the toggler.
const directionReverse = reverseDirectionMap[direction];
const dimension = dimensionMap[direction];
const directionBottomRight = direction === "bottom" || direction === "right";
const directionBottomRightMultiplier = directionBottomRight ? 1 : -1;
const secondaryDimensionAdjust = isPrimaryDimension ? 0 : toggler[dimension];
const spacingAdjust = isPrimaryDimension ? spacing : 0;
const offsetAdjust = isPrimaryDimension ? 0 : offset;
// Primary space is size of the ideal position on the screen.
// eg.: for direction = "right" primary space would be space between
// toggler right and viewport right.
let primarySpace = viewport[direction] - toggler[direction];
primarySpace = primarySpace + secondaryDimensionAdjust * directionBottomRightMultiplier;
primarySpace = primarySpace * directionBottomRightMultiplier;
primarySpace = primarySpace - spacingAdjust;
primarySpace = primarySpace - offsetAdjust;
primarySpace = Math.max(0, primarySpace);
const fitsInPrimarySpace = menu[dimension] <= primarySpace;
if (fitsInPrimarySpace) {
// eg.: direction = "right"
// menu left side is placed to right side of toggler
// menu.left = toggler.width
const distance = toggler[dimension] - secondaryDimensionAdjust + spacingAdjust + offsetAdjust;
return [directionReverse, distance];
}
let secondarySpace = toggler[directionReverse] - viewport[directionReverse];
secondarySpace = secondarySpace + secondaryDimensionAdjust * directionBottomRightMultiplier;
secondarySpace = secondarySpace * directionBottomRightMultiplier;
secondarySpace = secondarySpace - spacingAdjust;
secondarySpace = secondarySpace - offsetAdjust;
secondarySpace = Math.max(0, secondarySpace);
const fitsInSecondarySpace = menu[dimension] <= secondarySpace;
if (fitsInSecondarySpace) {
// eg.: direction = "right"
// menu right side is placed to left side of toggler
// menu.left = -menu.width
const distance = -menu[dimension] + secondaryDimensionAdjust - spacingAdjust - offsetAdjust;
return [directionReverse, distance];
}
const doesNotFitInViewport = menu[dimension] > viewport[dimension];
if (doesNotFitInViewport) {
// eg.: direction = "right"
// menu left side is always placed to left side of viewport
// menu.left = viewport.left - menu.left
const distance =
(viewport[directionReverse] - toggler[directionReverse]) * directionBottomRightMultiplier;
return [directionReverse, distance];
}
// eg.: direction = "right"
// menu right is placed to the same viewport side
// menu.right = toggler.right - menu.right
const distance = (toggler[direction] - viewport[direction]) * directionBottomRightMultiplier;
return [direction, distance];
}