2024-01-29 09:26:07 +08:00

431 lines
18 KiB
JavaScript

const oppositionPositions = {
top: 'bottom',
bottom: 'top',
left: 'right',
right: 'left'
};
const oppositeAligns = {
start: 'end',
center: 'center',
end: 'start'
};
const propToCompare = {
top: 'height',
bottom: 'height',
left: 'width',
right: 'width'
};
const transformOrigins = {
'bottom-start': 'top left',
bottom: 'top center',
'bottom-end': 'top right',
'top-start': 'bottom left',
top: 'bottom center',
'top-end': 'bottom right',
'right-start': 'top left',
right: 'center left',
'right-end': 'bottom left',
'left-start': 'top right',
left: 'center right',
'left-end': 'bottom right'
};
const overlapTransformOrigin = {
'bottom-start': 'bottom left',
bottom: 'bottom center',
'bottom-end': 'bottom right',
'top-start': 'top left',
top: 'top center',
'top-end': 'top right',
'right-start': 'top right',
right: 'center right',
'right-end': 'bottom right',
'left-start': 'top left',
left: 'center left',
'left-end': 'bottom left'
};
const oppositeAlignCssPositionProps = {
'bottom-start': 'right',
'bottom-end': 'left',
'top-start': 'right',
'top-end': 'left',
'right-start': 'bottom',
'right-end': 'top',
'left-start': 'bottom',
'left-end': 'top'
};
const keepOffsetDirection = {
top: true,
bottom: false,
left: true,
right: false // left--
};
const cssPositionToOppositeAlign = {
top: 'end',
bottom: 'start',
left: 'end',
right: 'start'
};
export function getPlacementAndOffsetOfFollower(placement, targetRect, followerRect, shift, flip, overlap) {
if (!flip || overlap) {
return { placement: placement, top: 0, left: 0 };
}
const [position, align] = placement.split('-');
let properAlign = align !== null && align !== void 0 ? align : 'center';
let properOffset = {
top: 0,
left: 0
};
const deriveOffset = (oppositeAlignCssSizeProp, alignCssPositionProp, offsetVertically) => {
let left = 0;
let top = 0;
const diff = followerRect[oppositeAlignCssSizeProp] -
targetRect[alignCssPositionProp] -
targetRect[oppositeAlignCssSizeProp];
if (diff > 0 && shift) {
if (offsetVertically) {
// screen border
// |-----------------------------------------|
// | | f | |
// | | o | |
// | | l | |
// | | l |---- |
// | | o |tar | |
// | | w |get | |
// | | e | | |
// | | r |---- |
// | ---- |
// |-----------------------------------------|
top = keepOffsetDirection[alignCssPositionProp] ? diff : -diff;
}
else {
// screen border
// |----------------------------------------|
// | |
// | ---------- |
// | | target | |
// | ----------------------------------
// | | follower |
// | ----------------------------------
// | |
// |----------------------------------------|
left = keepOffsetDirection[alignCssPositionProp] ? diff : -diff;
}
}
return {
left,
top
};
};
const offsetVertically = position === 'left' || position === 'right';
// choose proper placement for non-center align
if (properAlign !== 'center') {
const oppositeAlignCssPositionProp = oppositeAlignCssPositionProps[placement];
const currentAlignCssPositionProp = oppositionPositions[oppositeAlignCssPositionProp];
const oppositeAlignCssSizeProp = propToCompare[oppositeAlignCssPositionProp];
// if follower rect is larger than target rect in align direction
// ----------[ target ]---------|
// ----------[ follower ]
if (followerRect[oppositeAlignCssSizeProp] >
targetRect[oppositeAlignCssSizeProp]) {
if (
// current space is not enough
// ----------[ target ]---------|
// -------[ follower ]
targetRect[oppositeAlignCssPositionProp] +
targetRect[oppositeAlignCssSizeProp] <
followerRect[oppositeAlignCssSizeProp]) {
const followerOverTargetSize = (followerRect[oppositeAlignCssSizeProp] -
targetRect[oppositeAlignCssSizeProp]) /
2;
if (targetRect[oppositeAlignCssPositionProp] < followerOverTargetSize ||
targetRect[currentAlignCssPositionProp] < followerOverTargetSize) {
// opposite align has larger space
// -------[ target ]-----------|
// -------[ follower ]-|
if (targetRect[oppositeAlignCssPositionProp] <
targetRect[currentAlignCssPositionProp]) {
properAlign = oppositeAligns[align];
properOffset = deriveOffset(oppositeAlignCssSizeProp, currentAlignCssPositionProp, offsetVertically);
}
else {
// ----------------[ target ]----|
// --------[ follower ]----|
properOffset = deriveOffset(oppositeAlignCssSizeProp, oppositeAlignCssPositionProp, offsetVertically);
}
}
else {
// 'center' align is better
// ------------[ target ]--------|
// -------[ follower ]--|
properAlign = 'center';
}
}
}
else if (followerRect[oppositeAlignCssSizeProp] <
targetRect[oppositeAlignCssSizeProp]) {
// TODO: maybe center is better
if (targetRect[currentAlignCssPositionProp] < 0 &&
// opposite align has larger space
// ------------[ target ]
// ----------------[follower]
targetRect[oppositeAlignCssPositionProp] >
targetRect[currentAlignCssPositionProp]) {
properAlign = oppositeAligns[align];
}
}
}
else {
const possibleAlternativeAlignCssPositionProp1 = position === 'bottom' || position === 'top' ? 'left' : 'top';
const possibleAlternativeAlignCssPositionProp2 = oppositionPositions[possibleAlternativeAlignCssPositionProp1];
const alternativeAlignCssSizeProp = propToCompare[possibleAlternativeAlignCssPositionProp1];
const followerOverTargetSize = (followerRect[alternativeAlignCssSizeProp] -
targetRect[alternativeAlignCssSizeProp]) /
2;
if (
// center is not enough
// ----------- [ target ]--|
// -------[ follower ]
targetRect[possibleAlternativeAlignCssPositionProp1] <
followerOverTargetSize ||
targetRect[possibleAlternativeAlignCssPositionProp2] <
followerOverTargetSize) {
// alternative 2 position's space is larger
if (targetRect[possibleAlternativeAlignCssPositionProp1] >
targetRect[possibleAlternativeAlignCssPositionProp2]) {
properAlign =
cssPositionToOppositeAlign[possibleAlternativeAlignCssPositionProp1];
properOffset = deriveOffset(alternativeAlignCssSizeProp, possibleAlternativeAlignCssPositionProp1, offsetVertically);
}
else {
// alternative 1 position's space is larger
properAlign =
cssPositionToOppositeAlign[possibleAlternativeAlignCssPositionProp2];
properOffset = deriveOffset(alternativeAlignCssSizeProp, possibleAlternativeAlignCssPositionProp2, offsetVertically);
}
}
}
let properPosition = position;
if (
// space is not enough
targetRect[position] < followerRect[propToCompare[position]] &&
// opposite position's space is larger
targetRect[position] < targetRect[oppositionPositions[position]]) {
properPosition = oppositionPositions[position];
}
return {
placement: properAlign !== 'center'
? `${properPosition}-${properAlign}`
: properPosition,
left: properOffset.left,
top: properOffset.top
};
}
export function getProperTransformOrigin(placement, overlap) {
if (overlap)
return overlapTransformOrigin[placement];
return transformOrigins[placement];
}
// ------------
// | offset |
// | |
// | [target] |
// | |
// ------------
// TODO: refactor it to remove dup logic
export function getOffset(placement, offsetRect, targetRect, offsetTopToStandardPlacement, offsetLeftToStandardPlacement, overlap) {
if (overlap) {
switch (placement) {
case 'bottom-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height)}px`,
left: `${Math.round(targetRect.left - offsetRect.left)}px`,
transform: 'translateY(-100%)'
};
case 'bottom-end':
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width)}px`,
transform: 'translateX(-100%) translateY(-100%)'
};
case 'top-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top)}px`,
left: `${Math.round(targetRect.left - offsetRect.left)}px`,
transform: ''
};
case 'top-end':
return {
top: `${Math.round(targetRect.top - offsetRect.top)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width)}px`,
transform: 'translateX(-100%)'
};
case 'right-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width)}px`,
transform: 'translateX(-100%)'
};
case 'right-end':
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width)}px`,
transform: 'translateX(-100%) translateY(-100%)'
};
case 'left-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top)}px`,
left: `${Math.round(targetRect.left - offsetRect.left)}px`,
transform: ''
};
case 'left-end':
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height)}px`,
left: `${Math.round(targetRect.left - offsetRect.left)}px`,
transform: 'translateY(-100%)'
};
case 'top':
return {
top: `${Math.round(targetRect.top - offsetRect.top)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width / 2)}px`,
transform: 'translateX(-50%)'
};
case 'right':
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height / 2)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width)}px`,
transform: 'translateX(-100%) translateY(-50%)'
};
case 'left':
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height / 2)}px`,
left: `${Math.round(targetRect.left - offsetRect.left)}px`,
transform: 'translateY(-50%)'
};
case 'bottom':
default:
return {
top: `${Math.round(targetRect.top - offsetRect.top + targetRect.height)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + targetRect.width / 2)}px`,
transform: 'translateX(-50%) translateY(-100%)'
};
}
}
switch (placement) {
case 'bottom-start':
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + offsetLeftToStandardPlacement)}px`,
transform: ''
};
case 'bottom-end':
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width +
offsetLeftToStandardPlacement)}px`,
transform: 'translateX(-100%)'
};
case 'top-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top + offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + offsetLeftToStandardPlacement)}px`,
transform: 'translateY(-100%)'
};
case 'top-end':
return {
top: `${Math.round(targetRect.top - offsetRect.top + offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width +
offsetLeftToStandardPlacement)}px`,
transform: 'translateX(-100%) translateY(-100%)'
};
case 'right-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top + offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width +
offsetLeftToStandardPlacement)}px`,
transform: ''
};
case 'right-end':
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width +
offsetLeftToStandardPlacement)}px`,
transform: 'translateY(-100%)'
};
case 'left-start':
return {
top: `${Math.round(targetRect.top - offsetRect.top + offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + offsetLeftToStandardPlacement)}px`,
transform: 'translateX(-100%)'
};
case 'left-end':
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + offsetLeftToStandardPlacement)}px`,
transform: 'translateX(-100%) translateY(-100%)'
};
case 'top':
return {
top: `${Math.round(targetRect.top - offsetRect.top + offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width / 2 +
offsetLeftToStandardPlacement)}px`,
transform: 'translateY(-100%) translateX(-50%)'
};
case 'right':
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height / 2 +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width +
offsetLeftToStandardPlacement)}px`,
transform: 'translateY(-50%)'
};
case 'left':
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height / 2 +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left - offsetRect.left + offsetLeftToStandardPlacement)}px`,
transform: 'translateY(-50%) translateX(-100%)'
};
case 'bottom':
default:
return {
top: `${Math.round(targetRect.top -
offsetRect.top +
targetRect.height +
offsetTopToStandardPlacement)}px`,
left: `${Math.round(targetRect.left -
offsetRect.left +
targetRect.width / 2 +
offsetLeftToStandardPlacement)}px`,
transform: 'translateX(-50%)'
};
}
}