UNPKG

uikit

Version:

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

235 lines (201 loc) • 7.99 kB
import { hasClass } from './class'; import { dimensions, offset, offsetPosition } from './dimensions'; import { isVisible, parent, parents } from './filter'; import { clamp, findIndex, includes, intersectRect, isWindow, toFloat, toWindow, ucfirst, } from './lang'; import { css } from './style'; export function isInView(element, offsetTop = 0, offsetLeft = 0) { if (!isVisible(element)) { return false; } return intersectRect( ...overflowParents(element) .map((parent) => { const { top, left, bottom, right } = offsetViewport(parent); return { top: top - offsetTop, left: left - offsetLeft, bottom: bottom + offsetTop, right: right + offsetLeft, }; }) .concat(offset(element)), ); } export function scrollIntoView(element, { offset: offsetBy = 0 } = {}) { const parents = isVisible(element) ? scrollParents(element, false, ['hidden']) : []; return parents.reduce( (fn, scrollElement, i) => { const { scrollTop, scrollHeight, offsetHeight } = scrollElement; const viewport = offsetViewport(scrollElement); const maxScroll = scrollHeight - viewport.height; const { height: elHeight, top: elTop } = parents[i - 1] ? offsetViewport(parents[i - 1]) : offset(element); let top = Math.ceil(elTop - viewport.top - offsetBy + scrollTop); if (offsetBy > 0 && offsetHeight < elHeight + offsetBy) { top += offsetBy; } else { offsetBy = 0; } if (top > maxScroll) { offsetBy -= top - maxScroll; top = maxScroll; } else if (top < 0) { offsetBy -= top; top = 0; } return () => scrollTo(scrollElement, top - scrollTop, element, maxScroll).then(fn); }, () => Promise.resolve(), )(); function scrollTo(element, top, targetEl, maxScroll) { return new Promise((resolve) => { const scroll = element.scrollTop; const duration = getDuration(Math.abs(top)); const start = Date.now(); const isScrollingElement = scrollingElement(element) === element; const targetTop = offset(targetEl).top + (isScrollingElement ? 0 : scroll); let prev = 0; let frames = 15; (function step() { const percent = ease(clamp((Date.now() - start) / duration)); let diff = 0; if (parents[0] === element && scroll + top < maxScroll) { diff = offset(targetEl).top + (isScrollingElement ? 0 : element.scrollTop) - targetTop; const coverEl = getCoveringElement(targetEl); diff -= coverEl ? offset(coverEl).height : 0; } element.scrollTop = scroll + (top + diff) * percent; // scroll more if we have not reached our destination // if element changes position during scroll try another step if (percent === 1 && (prev === diff || !frames--)) { resolve(); } else { prev = diff; requestAnimationFrame(step); } })(); }); } function getDuration(dist) { return 40 * Math.pow(dist, 0.375); } function ease(k) { return 0.5 * (1 - Math.cos(Math.PI * k)); } } export function scrolledOver(element, startOffset = 0, endOffset = 0) { if (!isVisible(element)) { return 0; } const scrollElement = scrollParent(element, true); const { scrollHeight, scrollTop } = scrollElement; const { height: viewportHeight } = offsetViewport(scrollElement); const maxScroll = scrollHeight - viewportHeight; const elementOffsetTop = offsetPosition(element)[0] - offsetPosition(scrollElement)[0]; const start = Math.max(0, elementOffsetTop - viewportHeight + startOffset); const end = Math.min(maxScroll, elementOffsetTop + element.offsetHeight - endOffset); return start < end ? clamp((scrollTop - start) / (end - start)) : 1; } export function scrollParents(element, scrollable = false, props = []) { const scrollEl = scrollingElement(element); let ancestors = parents(element).reverse(); ancestors = ancestors.slice(ancestors.indexOf(scrollEl) + 1); const fixedIndex = findIndex(ancestors, (el) => css(el, 'position') === 'fixed'); if (~fixedIndex) { ancestors = ancestors.slice(fixedIndex); } return [scrollEl] .concat( ancestors.filter( (parent) => css(parent, 'overflow') .split(' ') .some((prop) => includes(['auto', 'scroll', ...props], prop)) && (!scrollable || parent.scrollHeight > offsetViewport(parent).height), ), ) .reverse(); } export function scrollParent(...args) { return scrollParents(...args)[0]; } export function overflowParents(element) { return scrollParents(element, false, ['hidden', 'clip']); } export function offsetViewport(scrollElement) { const window = toWindow(scrollElement); let viewportElement = scrollElement === scrollingElement(scrollElement) ? window : scrollElement; if (isWindow(viewportElement) && window.visualViewport) { let { height, width, scale, pageTop: top, pageLeft: left } = window.visualViewport; height = Math.round(height * scale); width = Math.round(width * scale); return { height, width, top, left, bottom: top + height, right: left + width }; } let rect = offset(viewportElement); if (css(viewportElement, 'display') === 'inline') { return rect; } for (let [prop, dir, start, end] of [ ['width', 'x', 'left', 'right'], ['height', 'y', 'top', 'bottom'], ]) { if (isWindow(viewportElement)) { // iOS 12 returns <body> as scrollingElement viewportElement = viewportElement.document; } else { rect[start] += toFloat(css(viewportElement, `border-${start}-width`)); } const subpixel = rect[prop] % 1; rect[prop] = rect[dir] = viewportElement[`client${ucfirst(prop)}`] - (subpixel ? (subpixel < 0.5 ? -subpixel : 1 - subpixel) : 0); rect[end] = rect[prop] + rect[start]; } return rect; } export function getCoveringElement(target) { const { left, width, top } = dimensions(target); for (const position of top ? [0, top] : [0]) { for (const el of toWindow(target).document.elementsFromPoint(left + width / 2, position)) { if ( !el.contains(target) && // If e.g. Offcanvas is not yet closed !hasClass(el, 'uk-togglable-leave') && ((hasPosition(el, 'fixed') && zIndex( parents(target) .reverse() .find( (parent) => !parent.contains(el) && !hasPosition(parent, 'static'), ), ) < zIndex(el)) || (hasPosition(el, 'sticky') && parent(el).contains(target))) ) { return el; } } } } function zIndex(element) { return toFloat(css(element, 'zIndex')); } function hasPosition(element, position) { return css(element, 'position') === position; } function scrollingElement(element) { return toWindow(element).document.scrollingElement; }