UNPKG

uikit

Version:

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

405 lines (301 loc) • 10.9 kB
import {css} from './style'; import {attr} from './attr'; import {isVisible} from './filter'; import {clamp, each, endsWith, includes, intersectRect, isDocument, isNumeric, isUndefined, isWindow, pointInRect, toFloat, toNode, ucfirst} from './lang'; const dirs = { width: ['x', 'left', 'right'], height: ['y', 'top', 'bottom'] }; export function positionAt(element, target, elAttach, targetAttach, elOffset, targetOffset, flip, boundary) { elAttach = getPos(elAttach); targetAttach = getPos(targetAttach); const flipped = {element: elAttach, target: targetAttach}; if (!element || !target) { return flipped; } const dim = getDimensions(element); const targetDim = getDimensions(target); const position = targetDim; moveTo(position, elAttach, dim, -1); moveTo(position, targetAttach, targetDim, 1); elOffset = getOffsets(elOffset, dim.width, dim.height); targetOffset = getOffsets(targetOffset, targetDim.width, targetDim.height); elOffset['x'] += targetOffset['x']; elOffset['y'] += targetOffset['y']; position.left += elOffset['x']; position.top += elOffset['y']; if (flip) { const boundaries = [getDimensions(getWindow(element))]; if (boundary) { boundaries.unshift(getDimensions(boundary)); } each(dirs, ([dir, align, alignFlip], prop) => { if (!(flip === true || includes(flip, dir))) { return; } boundaries.some(boundary => { const elemOffset = elAttach[dir] === align ? -dim[prop] : elAttach[dir] === alignFlip ? dim[prop] : 0; const targetOffset = targetAttach[dir] === align ? targetDim[prop] : targetAttach[dir] === alignFlip ? -targetDim[prop] : 0; if (position[align] < boundary[align] || position[align] + dim[prop] > boundary[alignFlip]) { const centerOffset = dim[prop] / 2; const centerTargetOffset = targetAttach[dir] === 'center' ? -targetDim[prop] / 2 : 0; return elAttach[dir] === 'center' && ( apply(centerOffset, centerTargetOffset) || apply(-centerOffset, -centerTargetOffset) ) || apply(elemOffset, targetOffset); } function apply(elemOffset, targetOffset) { const newVal = position[align] + elemOffset + targetOffset - elOffset[dir] * 2; if (newVal >= boundary[align] && newVal + dim[prop] <= boundary[alignFlip]) { position[align] = newVal; ['element', 'target'].forEach(el => { flipped[el][dir] = !elemOffset ? flipped[el][dir] : flipped[el][dir] === dirs[prop][1] ? dirs[prop][2] : dirs[prop][1]; }); return true; } } }); }); } offset(element, position); return flipped; } export function offset(element, coordinates) { element = toNode(element); if (coordinates) { const currentOffset = offset(element); const pos = css(element, 'position'); ['left', 'top'].forEach(prop => { if (prop in coordinates) { const value = css(element, prop); css(element, prop, coordinates[prop] - currentOffset[prop] + toFloat(pos === 'absolute' && value === 'auto' ? position(element)[prop] : value) ); } }); return; } return getDimensions(element); } function getDimensions(element) { element = toNode(element); if (!element) { return {}; } const {pageYOffset: top, pageXOffset: left} = getWindow(element); if (isWindow(element)) { const height = element.innerHeight; const width = element.innerWidth; return { top, left, height, width, bottom: top + height, right: left + width }; } let style, hidden; if (!isVisible(element) && css(element, 'display') === 'none') { style = attr(element, 'style'); hidden = attr(element, 'hidden'); attr(element, { style: `${style || ''};display:block !important;`, hidden: null }); } const rect = element.getBoundingClientRect(); if (!isUndefined(style)) { attr(element, {style, hidden}); } return { height: rect.height, width: rect.width, top: rect.top + top, left: rect.left + left, bottom: rect.bottom + top, right: rect.right + left }; } export function position(element) { element = toNode(element); const parent = element.offsetParent || getDocEl(element); const parentOffset = offset(parent); const {top, left} = ['top', 'left'].reduce((props, prop) => { const propName = ucfirst(prop); props[prop] -= parentOffset[prop] + toFloat(css(element, `margin${propName}`)) + toFloat(css(parent, `border${propName}Width`)); return props; }, offset(element)); return {top, left}; } export const height = dimension('height'); export const width = dimension('width'); function dimension(prop) { const propName = ucfirst(prop); return (element, value) => { element = toNode(element); if (isUndefined(value)) { if (isWindow(element)) { return element[`inner${propName}`]; } if (isDocument(element)) { const doc = element.documentElement; return Math.max(doc[`offset${propName}`], doc[`scroll${propName}`]); } value = css(element, prop); value = value === 'auto' ? element[`offset${propName}`] : toFloat(value) || 0; return value - boxModelAdjust(prop, element); } else { css(element, prop, !value && value !== 0 ? '' : +value + boxModelAdjust(prop, element) + 'px' ); } }; } export function boxModelAdjust(prop, element, sizing = 'border-box') { return css(element, 'boxSizing') === sizing ? dirs[prop].slice(1).map(ucfirst).reduce((value, prop) => value + toFloat(css(element, `padding${prop}`)) + toFloat(css(element, `border${prop}Width`)) , 0) : 0; } function moveTo(position, attach, dim, factor) { each(dirs, ([dir, align, alignFlip], prop) => { if (attach[dir] === alignFlip) { position[align] += dim[prop] * factor; } else if (attach[dir] === 'center') { position[align] += dim[prop] * factor / 2; } }); } function getPos(pos) { const x = /left|center|right/; const y = /top|center|bottom/; pos = (pos || '').split(' '); if (pos.length === 1) { pos = x.test(pos[0]) ? pos.concat(['center']) : y.test(pos[0]) ? ['center'].concat(pos) : ['center', 'center']; } return { x: x.test(pos[0]) ? pos[0] : 'center', y: y.test(pos[1]) ? pos[1] : 'center' }; } function getOffsets(offsets, width, height) { const [x, y] = (offsets || '').split(' '); return { x: x ? toFloat(x) * (endsWith(x, '%') ? width / 100 : 1) : 0, y: y ? toFloat(y) * (endsWith(y, '%') ? height / 100 : 1) : 0 }; } export function flipPosition(pos) { switch (pos) { case 'left': return 'right'; case 'right': return 'left'; case 'top': return 'bottom'; case 'bottom': return 'top'; default: return pos; } } export function isInView(element, topOffset = 0, leftOffset = 0) { if (!isVisible(element)) { return false; } element = toNode(element); const win = getWindow(element); const client = element.getBoundingClientRect(); const bounding = { top: -topOffset, left: -leftOffset, bottom: topOffset + height(win), right: leftOffset + width(win) }; return intersectRect(client, bounding) || pointInRect({x: client.left, y: client.top}, bounding); } export function scrolledOver(element, heightOffset = 0) { if (!isVisible(element)) { return 0; } element = toNode(element); const win = getWindow(element); const doc = getDocument(element); const elHeight = element.offsetHeight + heightOffset; const [top] = offsetPosition(element); const vp = height(win); const vh = vp + Math.min(0, top - vp); const diff = Math.max(0, vp - (height(doc) + heightOffset - (top + elHeight))); return clamp(((vh + win.pageYOffset - top) / ((vh + (elHeight - (diff < vp ? diff : 0))) / 100)) / 100); } export function scrollTop(element, top) { element = toNode(element); if (isWindow(element) || isDocument(element)) { const {scrollTo, pageXOffset} = getWindow(element); scrollTo(pageXOffset, top); } else { element.scrollTop = top; } } export function offsetPosition(element) { const offset = [0, 0]; do { offset[0] += element.offsetTop; offset[1] += element.offsetLeft; if (css(element, 'position') === 'fixed') { const win = getWindow(element); offset[0] += win.pageYOffset; offset[1] += win.pageXOffset; return offset; } } while ((element = element.offsetParent)); return offset; } export function toPx(value, property = 'width', element = window) { return isNumeric(value) ? +value : endsWith(value, 'vh') ? percent(height(getWindow(element)), value) : endsWith(value, 'vw') ? percent(width(getWindow(element)), value) : endsWith(value, '%') ? percent(getDimensions(element)[property], value) : toFloat(value); } function percent(base, value) { return base * toFloat(value) / 100; } function getWindow(element) { return isWindow(element) ? element : getDocument(element).defaultView; } function getDocument(element) { return toNode(element).ownerDocument; } function getDocEl(element) { return getDocument(element).documentElement; }