uikit
Version:
UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.
236 lines (194 loc) • 6.83 kB
JavaScript
import { offset } from './dimensions';
import { clamp, isArray, ucfirst } from './lang';
import { css } from './style';
import { offsetViewport, overflowParents } from './viewport';
const dirs = [
['width', 'x', 'left', 'right'],
['height', 'y', 'top', 'bottom'],
];
export function positionAt(element, target, options) {
options = {
attach: {
element: ['left', 'top'],
target: ['left', 'top'],
...options.attach,
},
offset: [0, 0],
placement: [],
...options,
};
if (!isArray(target)) {
target = [target, target];
}
offset(element, getPosition(element, target, options));
}
function getPosition(element, target, options) {
const position = attachTo(element, target, options);
const { boundary, viewportOffset = 0, placement } = options;
let offsetPosition = position;
for (const [i, [prop, , start, end]] of Object.entries(dirs)) {
const viewport = getViewport(element, target[i], viewportOffset, boundary, i);
if (isWithin(position, viewport, i)) {
continue;
}
let offsetBy = 0;
// Flip
if (placement[i] === 'flip') {
const attach = options.attach.target[i];
if (
(attach === end && position[end] <= viewport[end]) ||
(attach === start && position[start] >= viewport[start])
) {
continue;
}
offsetBy = flip(element, target, options, i)[start] - position[start];
const scrollArea = getScrollArea(element, target[i], viewportOffset, i);
if (!isWithin(applyOffset(position, offsetBy, i), scrollArea, i)) {
if (isWithin(position, scrollArea, i)) {
continue;
}
if (options.recursion) {
return false;
}
const newPos = flipAxis(element, target, options);
if (newPos && isWithin(newPos, scrollArea, 1 - i)) {
return newPos;
}
continue;
}
// Shift
} else if (placement[i] === 'shift') {
const targetDim = offset(target[i]);
const { offset: elOffset } = options;
offsetBy =
clamp(
clamp(position[start], viewport[start], viewport[end] - position[prop]),
targetDim[start] - position[prop] + elOffset[i],
targetDim[end] - elOffset[i],
) - position[start];
}
offsetPosition = applyOffset(offsetPosition, offsetBy, i);
}
return offsetPosition;
}
function attachTo(element, target, options) {
let { attach, offset: offsetBy } = {
attach: {
element: ['left', 'top'],
target: ['left', 'top'],
...options.attach,
},
offset: [0, 0],
...options,
};
let elOffset = offset(element);
for (const [i, [prop, , start, end]] of Object.entries(dirs)) {
const targetOffset =
attach.target[i] === attach.element[i] ? offsetViewport(target[i]) : offset(target[i]);
elOffset = applyOffset(
elOffset,
targetOffset[start] -
elOffset[start] +
moveBy(attach.target[i], end, targetOffset[prop]) -
moveBy(attach.element[i], end, elOffset[prop]) +
+offsetBy[i],
i,
);
}
return elOffset;
}
function applyOffset(position, offset, i) {
const [, dir, start, end] = dirs[i];
const newPos = { ...position };
newPos[start] = position[dir] = position[start] + offset;
newPos[end] += offset;
return newPos;
}
function moveBy(attach, end, dim) {
return attach === 'center' ? dim / 2 : attach === end ? dim : 0;
}
function getViewport(element, target, viewportOffset, boundary, i) {
let viewport = getIntersectionArea(...commonScrollParents(element, target).map(offsetViewport));
if (viewportOffset) {
viewport[dirs[i][2]] += viewportOffset;
viewport[dirs[i][3]] -= viewportOffset;
}
if (boundary) {
viewport = getIntersectionArea(
viewport,
offset(isArray(boundary) ? boundary[i] : boundary),
);
}
return viewport;
}
function getScrollArea(element, target, viewportOffset, i) {
const [prop, axis, start, end] = dirs[i];
const [scrollElement] = commonScrollParents(element, target);
const viewport = offsetViewport(scrollElement);
if (['auto', 'scroll'].includes(css(scrollElement, `overflow-${axis}`))) {
viewport[start] -= scrollElement[`scroll${ucfirst(start)}`];
viewport[end] = viewport[start] + scrollElement[`scroll${ucfirst(prop)}`];
}
viewport[start] += viewportOffset;
viewport[end] -= viewportOffset;
return viewport;
}
function commonScrollParents(element, target) {
return overflowParents(target).filter((parent) => parent.contains(element));
}
function getIntersectionArea(...rects) {
let area = {};
for (const rect of rects) {
for (const [, , start, end] of dirs) {
area[start] = Math.max(area[start] || 0, rect[start]);
area[end] = Math.min(...[area[end], rect[end]].filter(Boolean));
}
}
return area;
}
function isWithin(positionA, positionB, i) {
const [, , start, end] = dirs[i];
return positionA[start] >= positionB[start] && positionA[end] <= positionB[end];
}
function flip(element, target, { offset, attach }, i) {
return attachTo(element, target, {
attach: {
element: flipAttach(attach.element, i),
target: flipAttach(attach.target, i),
},
offset: flipOffset(offset, i),
});
}
function flipAxis(element, target, options) {
return getPosition(element, target, {
...options,
attach: {
element: options.attach.element.map(flipAttachAxis).reverse(),
target: options.attach.target.map(flipAttachAxis).reverse(),
},
offset: options.offset.reverse(),
placement: options.placement.reverse(),
recursion: true,
});
}
function flipAttach(attach, i) {
const newAttach = [...attach];
const index = dirs[i].indexOf(attach[i]);
if (~index) {
newAttach[i] = dirs[i][1 - (index % 2) + 2];
}
return newAttach;
}
function flipAttachAxis(prop) {
for (let i = 0; i < dirs.length; i++) {
const index = dirs[i].indexOf(prop);
if (~index) {
return dirs[1 - i][(index % 2) + 2];
}
}
}
function flipOffset(offset, i) {
offset = [...offset];
offset[i] *= -1;
return offset;
}