UNPKG

uikit

Version:

UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.

212 lines (179 loc) • 6.41 kB
import { offset } from './dimensions'; import { clamp, includes, ucfirst } from './lang'; import { offsetViewport, scrollParents } 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], ...options, }; const dim = options.flip ? attachToWithFlip(element, target, options) : attachTo(element, target, options); offset(element, dim); } function attachTo(element, target, options) { let { attach, offset: offsetBy } = { attach: { element: ['left', 'top'], target: ['left', 'top'], ...options.attach, }, offset: [0, 0], ...options, }; const position = offset(element); const targetOffset = offset(target); for (const [i, [prop, dir, start, end]] of Object.entries(dirs)) { position[start] = position[dir] = targetOffset[start] + moveBy(attach.target[i], end, targetOffset[prop]) - moveBy(attach.element[i], end, position[prop]) + +offsetBy[i]; position[end] = position[start] + position[prop]; } return position; } function attachToWithFlip(element, target, options) { const position = attachTo(element, target, options); const targetDim = offset(target); let { flip, attach: { element: elAttach, target: targetAttach }, offset: elOffset, boundary, viewport, viewportPadding, } = options; let viewports = scrollParents(element); if (boundary === target) { viewports = viewports.filter((viewport) => viewport !== boundary); } const [scrollElement] = viewports; viewports.push(viewport); const offsetPosition = { ...position }; for (const [i, [prop, dir, start, end]] of Object.entries(dirs)) { if (flip !== true && !includes(flip, dir)) { continue; } const willFlip = !intersectLine(position, targetDim, i) && intersectLine(position, targetDim, 1 - i); viewport = getIntersectionArea(...viewports.filter(Boolean).map(offsetViewport)); if (viewportPadding) { viewport[start] += viewportPadding; viewport[end] -= viewportPadding; } if (boundary && !willFlip && position[prop] <= offset(boundary)[prop]) { viewport = getIntersectionArea(viewport, offset(boundary)); } const isInStartBoundary = position[start] >= viewport[start]; const isInEndBoundary = position[end] <= viewport[end]; if (isInStartBoundary && isInEndBoundary) { continue; } let offsetBy; // Flip if (willFlip) { if ( (elAttach[i] === end && isInStartBoundary) || (elAttach[i] === start && isInEndBoundary) ) { continue; } offsetBy = (elAttach[i] === start ? -position[prop] : elAttach[i] === end ? position[prop] : 0) + (targetAttach[i] === start ? targetDim[prop] : targetAttach[i] === end ? -targetDim[prop] : 0) - elOffset[i] * 2; if ( !isInScrollArea( { ...position, [start]: position[start] + offsetBy, [end]: position[end] + offsetBy, }, scrollElement, i ) ) { if (isInScrollArea(position, scrollElement, i)) { continue; } if (options.recursion) { return false; } const newPos = attachToWithFlip(element, target, { ...options, attach: { element: elAttach.map(flipDir).reverse(), target: targetAttach.map(flipDir).reverse(), }, offset: elOffset.reverse(), flip: flip === true ? flip : [...flip, dirs[1 - i][1]], recursion: true, }); if (newPos && isInScrollArea(newPos, scrollElement, 1 - i)) { return newPos; } } // Move } else { offsetBy = clamp( clamp(position[start], viewport[start], viewport[end] - position[prop]), targetDim[start] - position[prop] + elOffset[i], targetDim[end] - elOffset[i] ) - position[start]; } offsetPosition[start] = position[dir] = position[start] + offsetBy; offsetPosition[end] += offsetBy; } return offsetPosition; } function moveBy(start, end, dim) { return start === 'center' ? dim / 2 : start === end ? dim : 0; } 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 isInScrollArea(position, scrollElement, dir) { const viewport = offsetViewport(scrollElement); const [prop, , start, end] = dirs[dir]; viewport[start] -= scrollElement[`scroll${ucfirst(start)}`]; viewport[end] = viewport[start] + scrollElement[`scroll${ucfirst(prop)}`]; return position[start] >= viewport[start] && position[end] <= viewport[end]; } function intersectLine(dimA, dimB, dir) { const [, , start, end] = dirs[dir]; return dimA[end] > dimB[start] && dimB[end] > dimA[start]; } function flipDir(prop) { for (let i = 0; i < dirs.length; i++) { const index = dirs[i].indexOf(prop); if (~index) { return dirs[1 - i][(index % 2) + 2]; } } }